feat(git): add rebase workflow with UI state handling

This introduces Git rebase commands (start, continue, abort) and
exposes whether a rebase is currently in progress via the status
payload. The UI now blocks committing during an active rebase and
shows a dedicated rebase notice with continue/abort actions.

- Add tauri commands for rebase operations and status detection
- Update frontend to render rebase state and controls
- Adjust conflict/resolution messaging and related UI styling
This commit is contained in:
Christoph Brandau
2026-07-04 00:34:36 +02:00
parent 1d67312ee4
commit 3c4425a408
9 changed files with 273 additions and 18 deletions
+88
View File
@@ -47,6 +47,7 @@ pub struct GitStatus {
pub behind: u32,
pub files: Vec<GitFileStatus>,
pub clean: bool,
pub rebase_in_progress: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -1119,6 +1120,72 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
Err(format!("Merge failed: {details}"))
}
#[tauri::command]
pub fn rebase_branch(path: String, branch: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let branch = branch.trim();
if branch.is_empty() {
return Err("Branch name must not be empty.".to_string());
}
let output = git_command()
.arg("-C")
.arg(&repo)
.args(["rebase", branch])
.output()
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
rebase_status_or_error(&repo, output, "Rebase failed", true)
}
#[tauri::command]
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
if !rebase_in_progress(&repo) {
return Err("No rebase is currently in progress.".to_string());
}
let output = git_command()
.arg("-C")
.arg(&repo)
.args(["rebase", "--continue"])
.env("GIT_EDITOR", "true")
.output()
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
rebase_status_or_error(&repo, output, "Rebase continue failed", false)
}
#[tauri::command]
pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
if !rebase_in_progress(&repo) {
return Err("No rebase is currently in progress.".to_string());
}
run_git(&repo, ["rebase", "--abort"])?;
status_for_repo(&repo)
}
fn rebase_status_or_error(
repo: &Path,
output: Output,
context: &str,
ok_if_rebase_in_progress: bool,
) -> Result<GitStatus, String> {
if output.status.success() {
return status_for_repo(repo);
}
let status = status_for_repo(repo)?;
if has_unresolved_conflicts(&status) || (ok_if_rebase_in_progress && status.rebase_in_progress)
{
return Ok(status);
}
Err(format!("{context}: {}", command_output_details(&output)))
}
#[tauri::command]
pub fn list_commits(path: String, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
let repo = resolve_repo(&path)?;
@@ -1953,9 +2020,30 @@ fn status_for_repo(repo: &Path) -> Result<GitStatus, String> {
behind: branch.behind,
clean: files.is_empty(),
files,
rebase_in_progress: rebase_in_progress(repo),
})
}
fn rebase_in_progress(repo: &Path) -> bool {
git_path_exists(repo, "rebase-merge") || git_path_exists(repo, "rebase-apply")
}
fn git_path_exists(repo: &Path, name: &str) -> bool {
let Ok(output) = run_git(repo, ["rev-parse", "--git-path", name]) else {
return false;
};
let value = String::from_utf8_lossy(&output).trim().to_string();
if value.is_empty() {
return false;
}
let path = PathBuf::from(value);
if path.is_absolute() {
path.exists()
} else {
repo.join(path).exists()
}
}
// `git status` only auto-detects renames between HEAD and the index (staged changes).
// A file renamed on disk but not yet `git add`ed shows up as a plain delete + untracked
// pair instead. We detect that case ourselves by comparing content hashes: if an unstaged
+7 -4
View File
@@ -11,10 +11,10 @@ use git::{
cred_delete, cred_load, cred_save, delete_branch, diff_file_against_working_tree, fetch,
get_file_patch, get_remote_url, get_status, list_branches, list_commits, list_file_history,
list_repository_files, list_stashes, merge_branch, open_repo_in_explorer, open_repository,
open_repository_bundle, open_repository_file, pull, push, read_conflict, rename_branch,
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop,
stash_push, unstage_files,
open_repository_bundle, open_repository_file, pull, push, read_conflict, rebase_abort,
rebase_branch, rebase_continue, rename_branch, resolve_conflict, resolve_conflict_side,
restore_file_from_commit, restore_files, restore_to_commit, search_code_introductions,
stage_files, stash_apply, stash_drop, stash_pop, stash_push, unstage_files,
};
fn main() {
@@ -55,6 +55,9 @@ fn main() {
restore_to_commit,
restore_file_from_commit,
merge_branch,
rebase_branch,
rebase_continue,
rebase_abort,
list_repository_files,
open_repository_bundle,
list_file_history,