Skip to content

Commit c7e920c

Browse files
committed
Implement commit and diff fix for new files
1 parent 065a651 commit c7e920c

3 files changed

Lines changed: 106 additions & 15 deletions

File tree

Backend/src/tauri_commands/commit.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ pub async fn commit_changes<R: Runtime>(
9595
}
9696

9797
#[tauri::command]
98-
/// Commits only selected file paths.
98+
/// Stages and commits only selected file paths.
9999
///
100100
/// # Parameters
101101
/// - `window`: Calling window handle for progress events.
@@ -147,7 +147,7 @@ pub async fn commit_selected<R: Runtime>(
147147
});
148148
let oid = repo
149149
.inner()
150-
.commit(&message, &name, &email, &paths)
150+
.commit_index(&message, &name, &email)
151151
.map_err(|e| {
152152
error!("Commit (selected) failed: {e}");
153153
e.to_string()
@@ -159,7 +159,7 @@ pub async fn commit_selected<R: Runtime>(
159159
}
160160

161161
#[tauri::command]
162-
/// Applies a patch to the index and creates a commit from staged hunks.
162+
/// Applies a patch to the index, stages selected files, and commits the staged index.
163163
///
164164
/// # Parameters
165165
/// - `window`: Calling window handle for progress events.
@@ -290,18 +290,15 @@ pub async fn commit_patch_and_files<R: Runtime>(
290290
e.to_string()
291291
})?;
292292
}
293-
let commit_paths: Vec<PathBuf> = if files.is_empty() {
294-
stage_paths.clone()
295-
} else {
296-
files.iter().map(PathBuf::from).collect()
297-
};
298-
let oid = if commit_paths.is_empty() {
293+
let has_selection = !patch.trim().is_empty() || !files.is_empty() || !stage_paths.is_empty();
294+
if !has_selection {
299295
return Err("No commit paths provided".into());
300-
} else {
301-
repo.inner()
302-
.commit(&message, &name, &email, &commit_paths)
303-
.map_err(|e| e.to_string())?
304-
};
296+
}
297+
298+
let oid = repo
299+
.inner()
300+
.commit_index(&message, &name, &email)
301+
.map_err(|e| e.to_string())?;
305302
on(VcsEvent::Info {
306303
msg: "Commit complete".into(),
307304
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright © 2025-2026 OpenVCS Contributors
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
vi.mock('../plugins', () => ({
6+
runHook: vi.fn(async () => ({ cancelled: false })),
7+
}));
8+
9+
vi.mock('../lib/tauri', () => {
10+
const invoke = vi.fn(async (cmd: string) => {
11+
if (cmd === 'commit_patch_and_files') return 'oid-123';
12+
return [];
13+
});
14+
return {
15+
TAURI: {
16+
invoke,
17+
listen: vi.fn(),
18+
},
19+
isTauriRuntimeAvailable: () => true,
20+
assertDesktopRuntime: () => {},
21+
__invoke: invoke,
22+
};
23+
});
24+
25+
vi.mock('./repo', () => ({
26+
hydrateStatus: vi.fn(async () => {}),
27+
hydrateCommits: vi.fn(async () => {}),
28+
}));
29+
30+
let state: typeof import('../state/state').state;
31+
32+
/** Mounts the minimal DOM needed for commit binding. */
33+
function mountCommitDom() {
34+
document.body.innerHTML = `
35+
<div id="status"></div>
36+
<input id="commit-summary" />
37+
<textarea id="commit-desc"></textarea>
38+
<button id="commit-btn"></button>
39+
`;
40+
}
41+
42+
beforeEach(async () => {
43+
vi.resetModules();
44+
mountCommitDom();
45+
state = (await import('../state/state')).state;
46+
state.files = [{ path: 'content/posts/2026/05/openvcs-announcement.md', status: '??' }] as any;
47+
state.selectedFiles = new Set(['content/posts/2026/05/openvcs-announcement.md']);
48+
state.selectedHunksByFile = {
49+
'content/posts/2026/05/openvcs-announcement.md': [0],
50+
} as any;
51+
state.selectedLinesByFile = {};
52+
state.selectedHunks = [];
53+
state.diffSelectedFiles = new Set();
54+
(state as any).branch = 'main';
55+
});
56+
57+
afterEach(() => {
58+
document.body.innerHTML = '';
59+
vi.restoreAllMocks();
60+
});
61+
62+
describe('bindCommit', () => {
63+
it('keeps untracked selected files in stage_paths', async () => {
64+
const { bindCommit } = await import('./diff');
65+
const commitSummary = document.getElementById('commit-summary') as HTMLInputElement;
66+
const commitBtn = document.getElementById('commit-btn') as HTMLButtonElement;
67+
68+
commitSummary.value = 'Add post';
69+
commitBtn.disabled = false;
70+
71+
bindCommit();
72+
commitBtn.click();
73+
74+
await Promise.resolve();
75+
76+
const { __invoke: invoke } = await import('../lib/tauri') as any;
77+
const call = (invoke as ReturnType<typeof vi.fn>).mock.calls.find((args: unknown[]) => args[0] === 'commit_patch_and_files');
78+
expect(call).toBeTruthy();
79+
expect(call?.[1]).toMatchObject({
80+
summary: 'Add post',
81+
patch: '',
82+
files: ['content/posts/2026/05/openvcs-announcement.md'],
83+
stagePaths: ['content/posts/2026/05/openvcs-announcement.md'],
84+
});
85+
});
86+
});

Frontend/src/scripts/features/diff.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,16 @@ export function bindCommit() {
3535
...Object.keys(linesMap).filter(p => linesMap[p] && Object.keys(linesMap[p] || {}).length > 0),
3636
]));
3737

38+
const selectedUntrackedFiles = new Set(
39+
(state.files || [])
40+
.filter((file: any) => String(file?.status || '').includes('?'))
41+
.map((file: any) => String(file?.path || ''))
42+
.filter(Boolean),
43+
);
44+
3845
// Full-file selections are staged directly; partial selections are staged via patch.
39-
const stagePaths = selectedFiles.filter(f => !partialFiles.includes(f));
46+
// Untracked files still need to be staged even if the UI has synthetic hunk state.
47+
const stagePaths = selectedFiles.filter(f => !partialFiles.includes(f) || selectedUntrackedFiles.has(f));
4048

4149
// Build patch only from hunk and line selections.
4250
let combinedPatch = '';

0 commit comments

Comments
 (0)