feat(git): refactor Git commands to use async/await pattern

This update modifies several Git command functions to utilize the async/await
pattern, enhancing performance and responsiveness during operations. The changes
allow for non-blocking execution of commands, improving the overall user experience.

- Converted multiple Git command functions to async
- Added error handling for async operations
- Updated related tests to support async execution
This commit is contained in:
Christoph Brandau
2026-07-09 19:17:02 +02:00
parent 0877d5a568
commit 710c6d4a53
2 changed files with 229 additions and 181 deletions
+4 -1
View File
@@ -96,7 +96,10 @@
"Bash(identify src-tauri/icons/GitCat.ico)",
"Bash(python3 -c \"import PIL; print\\(PIL.__version__\\)\")",
"Bash(python3 *)",
"Bash(xxd -l 16 src-tauri/icons/GitCat.ico)"
"Bash(xxd -l 16 src-tauri/icons/GitCat.ico)",
"Bash(pkg-config --exists openssl)",
"Bash(sudo apt-get install -y libssl-dev pkg-config)",
"Bash(dpkg -L libssl3t64)"
]
}
}
+63 -18
View File
@@ -789,7 +789,8 @@ fn validate_tag_ref_name(name: &str) -> Result<String, String> {
}
#[tauri::command]
pub fn stage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
pub async fn stage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
validate_files(&files)?;
@@ -814,10 +815,14 @@ pub fn stage_files(path: String, files: Vec<String>) -> Result<GitStatus, String
}
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not stage files: {err}"))?
}
#[tauri::command]
pub fn unstage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
pub async fn unstage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
validate_files(&files)?;
@@ -827,10 +832,14 @@ pub fn unstage_files(path: String, files: Vec<String>) -> Result<GitStatus, Stri
}
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not unstage files: {err}"))?
}
#[tauri::command]
pub fn restore_files(path: String, files: Vec<String>, staged: bool) -> Result<GitStatus, String> {
pub async fn restore_files(path: String, files: Vec<String>, staged: bool) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
validate_files(&files)?;
@@ -846,6 +855,9 @@ pub fn restore_files(path: String, files: Vec<String>, staged: bool) -> Result<G
}
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not restore files: {err}"))?
}
#[tauri::command]
@@ -1055,7 +1067,8 @@ pub fn apply_file_patch(
}
#[tauri::command]
pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
pub async fn commit(path: String, message: String) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
if message.trim().is_empty() {
return Err("Commit message must not be empty.".to_string());
@@ -1068,10 +1081,14 @@ pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
run_git(&repo, ["commit", "-m", message.as_str()])?;
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not commit: {err}"))?
}
#[tauri::command]
pub fn amend_commit(path: String, message: Option<String>) -> Result<GitStatus, String> {
pub async fn amend_commit(path: String, message: Option<String>) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
if verify_commit(&repo, "HEAD").is_err() {
return Err("There is no commit to amend.".to_string());
@@ -1096,6 +1113,9 @@ pub fn amend_commit(path: String, message: Option<String>) -> Result<GitStatus,
}
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not amend commit: {err}"))?
}
#[tauri::command]
@@ -1132,11 +1152,12 @@ pub fn undo_last_commit(path: String) -> Result<GitStatus, String> {
}
#[tauri::command]
pub fn pull(
pub async fn pull(
path: String,
username: Option<String>,
password: Option<String>,
) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let pull_args = ["pull", "--no-rebase", "--ff", "--no-edit"];
let output = match (username.as_deref(), password.as_deref()) {
@@ -1165,6 +1186,9 @@ pub fn pull(
return Err(format!("AUTH_FAILED:{details}"));
}
Err(format!("Git command failed: {details}"))
})
.await
.map_err(|err| format!("Could not pull: {err}"))?
}
#[tauri::command]
@@ -1203,11 +1227,12 @@ pub async fn fetch(
}
#[tauri::command]
pub fn push(
pub async fn push(
path: String,
username: Option<String>,
password: Option<String>,
) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let push_args = push_args_for_repo(&repo)?;
match (username.as_deref(), password.as_deref()) {
@@ -1219,6 +1244,9 @@ pub fn push(
}
}
status_for_repo(&repo)
})
.await
.map_err(|err| format!("Could not push: {err}"))?
}
// ── Credential storage (OS keychain) ────────────────────────────────────────
@@ -1388,7 +1416,8 @@ pub fn cred_delete(key: String) -> Result<(), String> {
}
#[tauri::command]
pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
pub async fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let branch = branch.trim();
if branch.is_empty() {
@@ -1425,10 +1454,14 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
};
Err(format!("Merge failed: {details}"))
})
.await
.map_err(|err| format!("Could not merge: {err}"))?
}
#[tauri::command]
pub fn rebase_branch(path: String, branch: String) -> Result<GitStatus, String> {
pub async fn rebase_branch(path: String, branch: String) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let branch = branch.trim();
if branch.is_empty() {
@@ -1443,6 +1476,9 @@ pub fn rebase_branch(path: String, branch: String) -> Result<GitStatus, String>
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
rebase_status_or_error(&repo, output, "Rebase failed", true)
})
.await
.map_err(|err| format!("Could not rebase: {err}"))?
}
#[tauri::command]
@@ -1494,7 +1530,8 @@ fn rebase_status_or_error(
}
#[tauri::command]
pub fn cherry_pick_commit(path: String, commit: String) -> Result<GitStatus, String> {
pub async fn cherry_pick_commit(path: String, commit: String) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let commit_hash = verify_commit(&repo, &commit)?;
@@ -1507,6 +1544,9 @@ pub fn cherry_pick_commit(path: String, commit: String) -> Result<GitStatus, Str
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
cherry_pick_status_or_error(&repo, output, "Cherry-pick failed")
})
.await
.map_err(|err| format!("Could not cherry-pick: {err}"))?
}
#[tauri::command]
@@ -4841,12 +4881,12 @@ mod tests {
}));
}
#[test]
#[tokio::test]
#[cfg_attr(
windows,
ignore = "Git for Windows can fail local pull tests with a sh signal pipe error"
)]
fn pull_merges_diverging_branch_instead_of_requiring_fast_forward() {
async fn pull_merges_diverging_branch_instead_of_requiring_fast_forward() {
let repo = init_temp_repo("pull_diverged");
commit_initial_file(&repo.path);
let base_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -4876,7 +4916,9 @@ mod tests {
],
);
let status = pull(repo.path.to_string_lossy().to_string(), None, None).unwrap();
let status = pull(repo.path.to_string_lossy().to_string(), None, None)
.await
.unwrap();
assert!(status.clean, "{:?}", status.files);
assert!(repo.path.join("remote.txt").exists());
@@ -4925,12 +4967,12 @@ mod tests {
);
}
#[test]
#[tokio::test]
#[cfg_attr(
windows,
ignore = "Git for Windows can fail local push tests with a sh signal pipe error"
)]
fn push_sets_upstream_for_branch_without_tracking_remote() {
async fn push_sets_upstream_for_branch_without_tracking_remote() {
let repo = init_temp_repo("push_sets_upstream_integration");
let remote = init_bare_temp_repo("push_sets_upstream_integration_remote");
commit_initial_file(&repo.path);
@@ -4947,7 +4989,9 @@ mod tests {
["remote", "add", "origin", remote.path.to_str().unwrap()],
);
let status = push(repo.path.to_string_lossy().to_string(), None, None).unwrap();
let status = push(repo.path.to_string_lossy().to_string(), None, None)
.await
.unwrap();
assert_eq!(
status.upstream.as_deref(),
@@ -5015,8 +5059,8 @@ mod tests {
assert_eq!(contents.replace("\r\n", "\n"), "resolved\n");
}
#[test]
fn commit_rejects_unresolved_merge_conflicts() {
#[tokio::test]
async fn commit_rejects_unresolved_merge_conflicts() {
let repo = init_temp_repo("commit_rejects_conflicts");
fs::write(repo.path.join("file.txt"), "base\n").expect("base file should be written");
run_git_test(&repo.path, ["add", "file.txt"]);
@@ -5042,6 +5086,7 @@ mod tests {
repo.path.to_string_lossy().to_string(),
"should not commit".to_string(),
)
.await
.unwrap_err();
assert!(err.contains("Merge conflicts"));