Skip to content

Commit f37e659

Browse files
committed
feat(git): implement missing backend commands for git-graph SDK
Add 7 missing Rust backend commands invoked by src/sdk/git-graph.ts: - git_checkout: checkout a branch, tag, or commit (branch.rs) - git_create_branch: create a new branch with optional start point (branch.rs) - git_delete_branch: delete a local branch with optional force flag (branch.rs) - git_reset: reset HEAD to a commit with soft/mixed/hard mode (branch.rs) - git_revert: revert a commit with conflict handling (branch.rs) - git_cherry_pick: cherry-pick a single commit with conflict handling (cherry_pick.rs) - git_rebase: simple non-interactive rebase onto a target (rebase.rs) Also adds: - git_get_commit_graph registration (existed in graph.rs but was unregistered) - git_get_commit_details command and supporting types (CommitDetails, CommitPerson, CommitDetailRef, CommitDiffStat, CommitDetailFile) - first_parent field to GraphOptions struct Fixes frontend SDK (git-graph.ts) to use correct existing command names (git_branches instead of git_get_branches, git_list_tags instead of git_get_tags) and camelCase parameter naming for Tauri v2 auto-conversion. Fixes CommitComparison struct serialization by adding serde(rename_all = "camelCase") so files_changed serializes as filesChanged matching frontend expectations in both git-graph.ts and tauri-api.ts.
1 parent 9f2480a commit f37e659

File tree

7 files changed

+519
-14
lines changed

7 files changed

+519
-14
lines changed

src-tauri/src/app/git_commands.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ macro_rules! git_commands {
3333
$crate::git::log::git_compare_commits,
3434
$crate::git::log::git_commit_graph,
3535
$crate::git::log::git_log_graph,
36+
// Git graph commands
37+
$crate::git::graph::git_get_commit_graph,
38+
$crate::git::log::git_get_commit_details,
3639
// Git stash commands
3740
$crate::git::stash::git_stashes,
3841
$crate::git::stash::git_stash_list,
@@ -58,12 +61,14 @@ macro_rules! git_commands {
5861
$crate::git::bisect::git_bisect_reset,
5962
// Git cherry-pick commands
6063
$crate::git::cherry_pick::git_commit_files,
64+
$crate::git::cherry_pick::git_cherry_pick,
6165
$crate::git::cherry_pick::git_cherry_pick_status,
6266
$crate::git::cherry_pick::git_cherry_pick_start,
6367
$crate::git::cherry_pick::git_cherry_pick_continue,
6468
$crate::git::cherry_pick::git_cherry_pick_skip,
6569
$crate::git::cherry_pick::git_cherry_pick_abort,
6670
// Git rebase commands
71+
$crate::git::rebase::git_rebase,
6772
$crate::git::rebase::git_rebase_commits,
6873
$crate::git::rebase::git_rebase_status,
6974
$crate::git::rebase::git_rebase_start,
@@ -137,6 +142,11 @@ macro_rules! git_commands {
137142
$crate::git::branch::git_branch_rename,
138143
$crate::git::branch::git_reset_soft,
139144
$crate::git::branch::git_clean,
145+
$crate::git::branch::git_checkout,
146+
$crate::git::branch::git_create_branch,
147+
$crate::git::branch::git_delete_branch,
148+
$crate::git::branch::git_reset,
149+
$crate::git::branch::git_revert,
140150
// Git repository watcher
141151
$crate::git::watcher::git_watch_repository,
142152
// Git line staging

src-tauri/src/git/branch.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,150 @@ pub async fn git_clean(path: String, files: Option<Vec<String>>) -> Result<(), S
174174
.map_err(|e| format!("Task join error: {}", e))?
175175
}
176176

177+
/// Checkout a branch, tag, or commit
178+
#[tauri::command]
179+
pub async fn git_checkout(path: Option<String>, r#ref: String) -> Result<(), String> {
180+
let path = path.unwrap_or_else(|| ".".to_string());
181+
tokio::task::spawn_blocking(move || {
182+
let repo_root = get_repo_root(&path)?;
183+
let repo_root_path = Path::new(&repo_root);
184+
185+
let output = git_command_with_timeout(&["checkout", &r#ref], repo_root_path)?;
186+
187+
if !output.status.success() {
188+
let stderr = String::from_utf8_lossy(&output.stderr);
189+
return Err(format!("Failed to checkout '{}': {}", r#ref, stderr));
190+
}
191+
192+
info!("[Git] Checked out '{}'", r#ref);
193+
Ok(())
194+
})
195+
.await
196+
.map_err(|e| format!("Task join error: {}", e))?
197+
}
198+
199+
/// Create a new branch
200+
#[tauri::command]
201+
pub async fn git_create_branch(
202+
path: Option<String>,
203+
name: String,
204+
start_point: Option<String>,
205+
) -> Result<(), String> {
206+
let path = path.unwrap_or_else(|| ".".to_string());
207+
tokio::task::spawn_blocking(move || {
208+
let repo_root = get_repo_root(&path)?;
209+
let repo_root_path = Path::new(&repo_root);
210+
211+
let mut args = vec!["branch".to_string(), name.clone()];
212+
if let Some(ref s) = start_point {
213+
args.push(s.clone());
214+
}
215+
let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
216+
217+
let output = git_command_with_timeout(&args_refs, repo_root_path)?;
218+
219+
if !output.status.success() {
220+
let stderr = String::from_utf8_lossy(&output.stderr);
221+
return Err(format!("Failed to create branch '{}': {}", name, stderr));
222+
}
223+
224+
info!("[Git] Created branch '{}'", name);
225+
Ok(())
226+
})
227+
.await
228+
.map_err(|e| format!("Task join error: {}", e))?
229+
}
230+
231+
/// Delete a local branch
232+
#[tauri::command]
233+
pub async fn git_delete_branch(
234+
path: Option<String>,
235+
name: String,
236+
force: Option<bool>,
237+
) -> Result<(), String> {
238+
let path = path.unwrap_or_else(|| ".".to_string());
239+
tokio::task::spawn_blocking(move || {
240+
let repo_root = get_repo_root(&path)?;
241+
let repo_root_path = Path::new(&repo_root);
242+
243+
let flag = if force.unwrap_or(false) { "-D" } else { "-d" };
244+
let output = git_command_with_timeout(&["branch", flag, &name], repo_root_path)?;
245+
246+
if !output.status.success() {
247+
let stderr = String::from_utf8_lossy(&output.stderr);
248+
return Err(format!("Failed to delete branch '{}': {}", name, stderr));
249+
}
250+
251+
info!("[Git] Deleted branch '{}'", name);
252+
Ok(())
253+
})
254+
.await
255+
.map_err(|e| format!("Task join error: {}", e))?
256+
}
257+
258+
/// Reset current HEAD to a specific commit
259+
#[tauri::command]
260+
pub async fn git_reset(
261+
path: Option<String>,
262+
hash: String,
263+
mode: Option<String>,
264+
) -> Result<(), String> {
265+
let path = path.unwrap_or_else(|| ".".to_string());
266+
tokio::task::spawn_blocking(move || {
267+
let repo_root = get_repo_root(&path)?;
268+
let repo_root_path = Path::new(&repo_root);
269+
270+
let mode_str = mode.unwrap_or_else(|| "mixed".to_string());
271+
let mode_flag = match mode_str.as_str() {
272+
"soft" => "--soft",
273+
"mixed" => "--mixed",
274+
"hard" => "--hard",
275+
other => return Err(format!("Invalid reset mode: {}", other)),
276+
};
277+
278+
let output = git_command_with_timeout(&["reset", mode_flag, &hash], repo_root_path)?;
279+
280+
if !output.status.success() {
281+
let stderr = String::from_utf8_lossy(&output.stderr);
282+
return Err(format!("Failed to reset to '{}': {}", hash, stderr));
283+
}
284+
285+
info!("[Git] Reset ({}) to {}", mode_str, hash);
286+
Ok(())
287+
})
288+
.await
289+
.map_err(|e| format!("Task join error: {}", e))?
290+
}
291+
292+
/// Revert a commit
293+
#[tauri::command]
294+
pub async fn git_revert(path: Option<String>, hash: String) -> Result<(), String> {
295+
let path = path.unwrap_or_else(|| ".".to_string());
296+
tokio::task::spawn_blocking(move || {
297+
let repo_root = get_repo_root(&path)?;
298+
let repo_root_path = Path::new(&repo_root);
299+
300+
let output = git_command_with_timeout(&["revert", "--no-edit", &hash], repo_root_path)?;
301+
302+
if !output.status.success() {
303+
let stderr = String::from_utf8_lossy(&output.stderr);
304+
if stderr.contains("CONFLICT") || stderr.contains("conflict") {
305+
info!(
306+
"[Git] Revert of {} has conflicts, waiting for resolution",
307+
hash
308+
);
309+
return Ok(());
310+
}
311+
return Err(format!("Failed to revert '{}': {}", hash, stderr));
312+
}
313+
314+
info!("[Git] Reverted commit {}", hash);
315+
Ok(())
316+
})
317+
.await
318+
.map_err(|e| format!("Task join error: {}", e))?
319+
}
320+
177321
#[cfg(test)]
178322
#[allow(clippy::unwrap_used, clippy::expect_used)]
179323
mod tests {

src-tauri/src/git/cherry_pick.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,31 @@ pub async fn git_cherry_pick_status(path: String) -> Result<CherryPickStatus, St
156156
.map_err(|e| format!("Task join error: {}", e))?
157157
}
158158

159+
/// Cherry-pick a single commit (simple wrapper for git-graph SDK)
160+
#[tauri::command]
161+
pub async fn git_cherry_pick(path: Option<String>, hash: String) -> Result<(), String> {
162+
let path = path.unwrap_or_else(|| ".".to_string());
163+
tokio::task::spawn_blocking(move || {
164+
info!("Cherry-picking commit {}", hash);
165+
166+
let output = git_command_with_timeout(&["cherry-pick", &hash], Path::new(&path))?;
167+
168+
if !output.status.success() {
169+
let stderr = String::from_utf8_lossy(&output.stderr);
170+
if stderr.contains("CONFLICT") || stderr.contains("conflict") {
171+
info!("Cherry-pick has conflicts, waiting for resolution");
172+
return Ok(());
173+
}
174+
return Err(format!("Cherry-pick failed: {}", stderr));
175+
}
176+
177+
info!("Cherry-pick of {} completed successfully", hash);
178+
Ok(())
179+
})
180+
.await
181+
.map_err(|e| format!("Task join error: {}", e))?
182+
}
183+
159184
/// Start a cherry-pick operation for one or more commits
160185
#[tauri::command]
161186
pub async fn git_cherry_pick_start(path: String, commits: Vec<String>) -> Result<(), String> {

0 commit comments

Comments
 (0)