refactor(git): improve interactive rebase workflow robustness
This commit removes the dedicated feature QA page and significantly refactors the internal Git library logic for handling complex workflows, particularly interactive rebase. The changes introduce helper functions to manage temporary files and ensure proper cleanup of rebase artifacts regardless of success or failure. This improves the reliability of advanced git operations within the application. - Added comprehensive cleanup routines for rebase helpers - Centralized constants for rebase file names - Removed obsolete QA feature code
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<!doctype html><html data-theme="light"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Git features QA</title><script type="module" src="/src/feature-qa.ts"></script></head><body><div id="qa"></div></body></html>
|
||||
+68
-24
@@ -210,6 +210,10 @@ pub struct ReflogEntry {
|
||||
|
||||
const SEQUENCE_EDITOR_PLAN_ENV: &str = "GITTY_SEQUENCE_EDITOR_PLAN";
|
||||
const COMMIT_EDITOR_QUEUE_ENV: &str = "GITTY_COMMIT_EDITOR_QUEUE";
|
||||
const REBASE_TODO_FILE: &str = "gitty-interactive-rebase-todo";
|
||||
const REWORD_QUEUE_FILE: &str = "gitty-interactive-rebase-messages";
|
||||
const SEQUENCE_HELPER_STEM: &str = ".gitty-sequence-editor";
|
||||
const COMMIT_HELPER_STEM: &str = ".gitty-commit-editor";
|
||||
|
||||
pub fn run_sequence_editor_if_requested() -> Option<Result<(), String>> {
|
||||
let executable = env::current_exe().ok()?;
|
||||
@@ -1632,33 +1636,24 @@ pub async fn start_interactive_rebase(
|
||||
let todo = build_rebase_todo(&available, &plan)?;
|
||||
let reword_queue = build_reword_queue(&available, &plan)?;
|
||||
let git_dir = git_dir_for_repo(&repo)?;
|
||||
let todo_path = git_dir.join("gitty-interactive-rebase-todo");
|
||||
let reword_queue_path = git_dir.join("gitty-interactive-rebase-messages");
|
||||
cleanup_interactive_rebase_helpers(&repo);
|
||||
let todo_path = git_dir.join(REBASE_TODO_FILE);
|
||||
let reword_queue_path = git_dir.join(REWORD_QUEUE_FILE);
|
||||
fs::write(&todo_path, todo)
|
||||
.map_err(|err| format!("Could not prepare interactive rebase plan: {err}"))?;
|
||||
fs::write(&reword_queue_path, reword_queue)
|
||||
.map_err(|err| format!("Could not prepare reword messages: {err}"))?;
|
||||
|
||||
let sequence_helper_name = if cfg!(windows) {
|
||||
format!(".gitty-sequence-editor-{}.exe", std::process::id())
|
||||
} else {
|
||||
format!(".gitty-sequence-editor-{}", std::process::id())
|
||||
};
|
||||
let commit_helper_name = if cfg!(windows) {
|
||||
format!(".gitty-commit-editor-{}.exe", std::process::id())
|
||||
} else {
|
||||
format!(".gitty-commit-editor-{}", std::process::id())
|
||||
};
|
||||
let sequence_helper_path = repo.join(&sequence_helper_name);
|
||||
let commit_helper_path = repo.join(&commit_helper_name);
|
||||
let sequence_helper_path = repo.join(sequence_helper_name());
|
||||
let commit_helper_path = repo.join(commit_helper_name());
|
||||
let current_exe = env::current_exe()
|
||||
.map_err(|err| format!("Could not locate the Gitty executable: {err}"))?;
|
||||
fs::copy(¤t_exe, &sequence_helper_path)
|
||||
.and_then(|_| fs::copy(¤t_exe, &commit_helper_path))
|
||||
.map_err(|err| format!("Could not prepare interactive rebase helpers: {err}"))?;
|
||||
|
||||
let sequence_editor_command = format!("./{sequence_helper_name}");
|
||||
let commit_editor_command = format!("./{commit_helper_name}");
|
||||
let sequence_editor_command = format!("./{}", sequence_helper_name());
|
||||
let commit_editor_command = format!("./{}", commit_helper_name());
|
||||
let output = git_command()
|
||||
.arg("-C")
|
||||
.arg(&repo)
|
||||
@@ -1670,11 +1665,14 @@ pub async fn start_interactive_rebase(
|
||||
.output()
|
||||
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"));
|
||||
|
||||
let result = rebase_status_or_error(&repo, output?, "Interactive rebase failed", true);
|
||||
let _ = fs::remove_file(&todo_path);
|
||||
let _ = fs::remove_file(&reword_queue_path);
|
||||
let _ = fs::remove_file(&sequence_helper_path);
|
||||
if !matches!(&result, Ok(status) if status.rebase_in_progress) {
|
||||
let _ = fs::remove_file(&reword_queue_path);
|
||||
let _ = fs::remove_file(&commit_helper_path);
|
||||
rebase_status_or_error(&repo, output?, "Interactive rebase failed", true)
|
||||
}
|
||||
result
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
|
||||
@@ -1687,15 +1685,27 @@ pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
||||
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")
|
||||
let git_dir = git_dir_for_repo(&repo)?;
|
||||
let queue_path = git_dir.join(REWORD_QUEUE_FILE);
|
||||
let helper_path = repo.join(commit_helper_name());
|
||||
let mut command = git_command();
|
||||
command.arg("-C").arg(&repo).args(["rebase", "--continue"]);
|
||||
if queue_path.exists() && helper_path.exists() {
|
||||
command
|
||||
.env("GIT_EDITOR", format!("./{}", commit_helper_name()))
|
||||
.env(COMMIT_EDITOR_QUEUE_ENV, &queue_path);
|
||||
} else {
|
||||
command.env("GIT_EDITOR", "true");
|
||||
}
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||
|
||||
rebase_status_or_error(&repo, output, "Rebase continue failed", false)
|
||||
let result = rebase_status_or_error(&repo, output, "Rebase continue failed", false);
|
||||
if !matches!(&result, Ok(status) if status.rebase_in_progress) {
|
||||
cleanup_interactive_rebase_helpers(&repo);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1706,6 +1716,7 @@ pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
||||
}
|
||||
|
||||
run_git(&repo, ["rebase", "--abort"])?;
|
||||
cleanup_interactive_rebase_helpers(&repo);
|
||||
status_for_repo(&repo)
|
||||
}
|
||||
|
||||
@@ -1911,6 +1922,38 @@ fn git_dir_for_repo(repo: &Path) -> Result<PathBuf, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn sequence_helper_name() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
".gitty-sequence-editor.exe"
|
||||
} else {
|
||||
SEQUENCE_HELPER_STEM
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_helper_name() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
".gitty-commit-editor.exe"
|
||||
} else {
|
||||
COMMIT_HELPER_STEM
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_interactive_rebase_helpers(repo: &Path) {
|
||||
if let Ok(git_dir) = git_dir_for_repo(repo) {
|
||||
let _ = fs::remove_file(git_dir.join(REBASE_TODO_FILE));
|
||||
let _ = fs::remove_file(git_dir.join(REWORD_QUEUE_FILE));
|
||||
}
|
||||
let _ = fs::remove_file(repo.join(sequence_helper_name()));
|
||||
let _ = fs::remove_file(repo.join(commit_helper_name()));
|
||||
}
|
||||
|
||||
fn is_interactive_rebase_helper_path(path: &str) -> bool {
|
||||
matches!(
|
||||
path.replace('\\', "/").rsplit('/').next(),
|
||||
Some(name) if name == sequence_helper_name() || name == commit_helper_name()
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_reflog(output: &[u8]) -> Result<Vec<ReflogEntry>, String> {
|
||||
let mut entries = Vec::new();
|
||||
for raw in output.split(|byte| *byte == 0x1e) {
|
||||
@@ -2946,6 +2989,7 @@ fn status_for_repo(repo: &Path) -> Result<GitStatus, String> {
|
||||
)?;
|
||||
let (branch, mut files) = parse_status_output(&output)?;
|
||||
detect_worktree_renames(repo, &mut files);
|
||||
files.retain(|file| !is_interactive_rebase_helper_path(&file.path));
|
||||
|
||||
Ok(GitStatus {
|
||||
repo_path: repo.to_string_lossy().to_string(),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
|
||||
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
|
||||
|
||||
const target = document.getElementById("qa")!;
|
||||
const branches = [
|
||||
{ name: "features/rewrite-history", current: true, remote: false },
|
||||
{ name: "main", current: false, remote: false },
|
||||
{ name: "origin/main", current: false, remote: true },
|
||||
];
|
||||
const commits = [
|
||||
{ hash: "1111111111111111111111111111111111111111", short_hash: "1111111", summary: "Add reflog backend", author_name: "Ada", date: "2026-07-10T09:10:00+02:00" },
|
||||
{ hash: "2222222222222222222222222222222222222222", short_hash: "2222222", summary: "Build interactive rebase dialog", author_name: "Linus", date: "2026-07-10T10:20:00+02:00" },
|
||||
{ hash: "3333333333333333333333333333333333333333", short_hash: "3333333", summary: "Polish recovery workflow", author_name: "Grace", date: "2026-07-10T11:30:00+02:00" },
|
||||
];
|
||||
const entries = [
|
||||
{ hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", short_hash: "aaaaaaa", selector: "HEAD@{0}", action: "commit: Add recovery workflow", author_name: "Ada", date: "2026-07-10T12:00:00+02:00" },
|
||||
{ hash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", short_hash: "bbbbbbb", selector: "HEAD@{1}", action: "rebase (finish): returning to refs/heads/feature", author_name: "Ada", date: "2026-07-10T11:00:00+02:00" },
|
||||
{ hash: "cccccccccccccccccccccccccccccccccccccccc", short_hash: "ccccccc", selector: "HEAD@{2}", action: "checkout: moving from main to feature", author_name: "Ada", date: "2026-07-10T10:00:00+02:00" },
|
||||
];
|
||||
|
||||
if (location.hash === "#reflog") {
|
||||
mount(ReflogDialog, { target, props: { entries, currentHash: entries[0].hash, isLoading: false, isBusy: false, operation: "", error: "", onPreview: () => {}, onRestore: (_entry, branch) => { document.title = `Recovered ${branch}`; }, onClose: () => {} } });
|
||||
} else {
|
||||
mount(InteractiveRebaseDialog, { target, props: { branches, currentBranch: branches[0].name, base: "main", commits, isLoading: false, isBusy: false, operation: "", error: "", onBaseChange: () => {}, onStart: (plan) => { document.title = `Rebase ${plan.length} commits`; }, onClose: () => {} } });
|
||||
}
|
||||
Reference in New Issue
Block a user