Merge pull request 'Bisect' (#31) from bisect into main
Reviewed-on: #31
This commit was merged in pull request #31.
This commit is contained in:
@@ -131,6 +131,26 @@ pub struct GitCommit {
|
|||||||
pub has_note: bool,
|
pub has_note: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BisectCommit {
|
||||||
|
pub hash: String,
|
||||||
|
pub short_hash: String,
|
||||||
|
pub summary: String,
|
||||||
|
pub author_name: String,
|
||||||
|
pub date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct BisectState {
|
||||||
|
pub active: bool,
|
||||||
|
pub finished: bool,
|
||||||
|
pub current: Option<BisectCommit>,
|
||||||
|
pub culprit: Option<BisectCommit>,
|
||||||
|
pub remaining_revisions: Option<u32>,
|
||||||
|
pub remaining_steps: Option<u32>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct GitCommitFile {
|
pub struct GitCommitFile {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -3377,6 +3397,181 @@ pub fn restore_reflog_entry(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bisect_in_progress(repo: &Path) -> Result<bool, String> {
|
||||||
|
Ok(git_dir_for_repo(repo)?.join("BISECT_START").is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bisect_commit_for_ref(repo: &Path, revision: &str) -> Result<BisectCommit, String> {
|
||||||
|
let output = run_git(
|
||||||
|
repo,
|
||||||
|
[
|
||||||
|
"log",
|
||||||
|
"-1",
|
||||||
|
"--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI",
|
||||||
|
revision,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
let text = String::from_utf8_lossy(&output);
|
||||||
|
let fields = text.trim().splitn(5, '\x1f').collect::<Vec<_>>();
|
||||||
|
if fields.len() != 5 {
|
||||||
|
return Err("Git returned an unexpected bisect commit record.".to_string());
|
||||||
|
}
|
||||||
|
Ok(BisectCommit {
|
||||||
|
hash: fields[0].to_string(),
|
||||||
|
short_hash: fields[1].to_string(),
|
||||||
|
summary: fields[2].to_string(),
|
||||||
|
author_name: fields[3].to_string(),
|
||||||
|
date: fields[4].to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_bisect_progress(message: &str) -> (Option<u32>, Option<u32>) {
|
||||||
|
let Some(line) = message.lines().find(|line| line.contains("Bisecting:")) else {
|
||||||
|
return (None, None);
|
||||||
|
};
|
||||||
|
let revisions = line.split("Bisecting:").nth(1).and_then(|tail| {
|
||||||
|
tail.split_whitespace()
|
||||||
|
.find_map(|word| word.parse::<u32>().ok())
|
||||||
|
});
|
||||||
|
let steps = line.split("roughly").nth(1).and_then(|tail| {
|
||||||
|
tail.split_whitespace()
|
||||||
|
.find_map(|word| word.parse::<u32>().ok())
|
||||||
|
});
|
||||||
|
(revisions, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_bisect_culprit(message: &str) -> Option<String> {
|
||||||
|
for line in message.lines() {
|
||||||
|
if line.contains(" is the first ") && line.contains("bad") && line.contains(" commit") {
|
||||||
|
return line.split_whitespace().next().map(ToString::to_string);
|
||||||
|
}
|
||||||
|
if line.contains("first") && line.contains("bad") && line.contains("commit:") {
|
||||||
|
let start = line.find('[')? + 1;
|
||||||
|
let end = line[start..].find(']')? + start;
|
||||||
|
return Some(line[start..end].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bisect_state_for_repo(repo: &Path, message: String) -> Result<BisectState, String> {
|
||||||
|
let active = bisect_in_progress(repo)?;
|
||||||
|
if !active {
|
||||||
|
return Ok(BisectState {
|
||||||
|
active: false,
|
||||||
|
finished: false,
|
||||||
|
current: None,
|
||||||
|
culprit: None,
|
||||||
|
remaining_revisions: None,
|
||||||
|
remaining_steps: None,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let log = run_git(repo, ["bisect", "log"])
|
||||||
|
.map(|output| String::from_utf8_lossy(&output).into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let culprit_hash = parse_bisect_culprit(&message).or_else(|| parse_bisect_culprit(&log));
|
||||||
|
let current = bisect_commit_for_ref(repo, "HEAD")?;
|
||||||
|
let culprit = culprit_hash
|
||||||
|
.as_deref()
|
||||||
|
.map(|hash| bisect_commit_for_ref(repo, hash))
|
||||||
|
.transpose()?;
|
||||||
|
let (remaining_revisions, remaining_steps) = parse_bisect_progress(&message);
|
||||||
|
Ok(BisectState {
|
||||||
|
active,
|
||||||
|
finished: culprit.is_some(),
|
||||||
|
current: Some(current),
|
||||||
|
culprit,
|
||||||
|
remaining_revisions,
|
||||||
|
remaining_steps,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_bisect_state(path: String) -> Result<BisectState, String> {
|
||||||
|
run_git_task("Could not inspect Git bisect", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
bisect_state_for_repo(&repo, String::new())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_bisect(path: String, good: String, bad: String) -> Result<BisectState, String> {
|
||||||
|
run_git_task("Could not start Git bisect", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
if bisect_in_progress(&repo)? {
|
||||||
|
return Err("A Git bisect session is already active.".to_string());
|
||||||
|
}
|
||||||
|
let status = status_for_repo(&repo)?;
|
||||||
|
if !status.clean {
|
||||||
|
return Err(
|
||||||
|
"Commit or stash working tree changes before starting Git bisect.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if status.rebase_in_progress || status.cherry_pick_in_progress || status.merge_in_progress {
|
||||||
|
return Err("Finish the current Git operation before starting Git bisect.".to_string());
|
||||||
|
}
|
||||||
|
let good_hash = verify_commit(&repo, &good)?;
|
||||||
|
let bad_hash = verify_commit(&repo, &bad)?;
|
||||||
|
if good_hash == bad_hash {
|
||||||
|
return Err("Good and bad must reference different commits.".to_string());
|
||||||
|
}
|
||||||
|
let ancestry = git_command()
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&repo)
|
||||||
|
.args([
|
||||||
|
"merge-base",
|
||||||
|
"--is-ancestor",
|
||||||
|
good_hash.as_str(),
|
||||||
|
bad_hash.as_str(),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
if !ancestry.status.success() {
|
||||||
|
return Err("The good commit must be an ancestor of the bad commit.".to_string());
|
||||||
|
}
|
||||||
|
let output = run_git(
|
||||||
|
&repo,
|
||||||
|
["bisect", "start", bad_hash.as_str(), good_hash.as_str()],
|
||||||
|
)?;
|
||||||
|
bisect_state_for_repo(&repo, String::from_utf8_lossy(&output).trim().to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn mark_bisect(path: String, verdict: String) -> Result<BisectState, String> {
|
||||||
|
run_git_task("Could not continue Git bisect", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
if !bisect_in_progress(&repo)? {
|
||||||
|
return Err("No Git bisect session is active.".to_string());
|
||||||
|
}
|
||||||
|
let verdict = match verdict.as_str() {
|
||||||
|
"good" => "good",
|
||||||
|
"bad" => "bad",
|
||||||
|
"skip" => "skip",
|
||||||
|
_ => return Err("Bisect verdict must be good, bad, or skip.".to_string()),
|
||||||
|
};
|
||||||
|
let output = run_git(&repo, ["bisect", verdict])?;
|
||||||
|
bisect_state_for_repo(&repo, String::from_utf8_lossy(&output).trim().to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_bisect(path: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not stop Git bisect", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
if bisect_in_progress(&repo)? {
|
||||||
|
run_git(&repo, ["bisect", "reset"])?;
|
||||||
|
}
|
||||||
|
status_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
fn interactive_rebase_commits_for_repo(
|
fn interactive_rebase_commits_for_repo(
|
||||||
repo: &Path,
|
repo: &Path,
|
||||||
base: &str,
|
base: &str,
|
||||||
@@ -10117,4 +10312,69 @@ mod tests {
|
|||||||
assert_eq!(git_output_test(&repo.path, ["ls-files"]), "keep.txt");
|
assert_eq!(git_output_test(&repo.path, ["ls-files"]), "keep.txt");
|
||||||
assert!(repo.path.join("keep.txt").is_file());
|
assert!(repo.path.join("keep.txt").is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_bisect_progress_and_culprit_output() {
|
||||||
|
let message = "Bisecting: 7 revisions left to test after this (roughly 3 steps)\nabc123 is the first 'bad' commit";
|
||||||
|
assert_eq!(parse_bisect_progress(message), (Some(7), Some(3)));
|
||||||
|
assert_eq!(parse_bisect_culprit(message).as_deref(), Some("abc123"));
|
||||||
|
assert_eq!(
|
||||||
|
parse_bisect_culprit("# first 'bad' commit: [def456] broken behavior").as_deref(),
|
||||||
|
Some("def456")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bisect_state_tracks_a_complete_session_and_reset() {
|
||||||
|
let repo = init_temp_repo("bisect_session");
|
||||||
|
let mut hashes = Vec::new();
|
||||||
|
for index in 0..6 {
|
||||||
|
fs::write(repo.path.join("value.txt"), format!("{index}\n"))
|
||||||
|
.expect("fixture should be written");
|
||||||
|
run_git_test(&repo.path, ["add", "value.txt"]);
|
||||||
|
run_git_test(
|
||||||
|
&repo.path,
|
||||||
|
["commit", "-q", "-m", &format!("commit {index}")],
|
||||||
|
);
|
||||||
|
hashes.push(git_output_test(&repo.path, ["rev-parse", "HEAD"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = run_git(
|
||||||
|
&repo.path,
|
||||||
|
["bisect", "start", hashes[5].as_str(), hashes[0].as_str()],
|
||||||
|
)
|
||||||
|
.expect("bisect should start");
|
||||||
|
let mut state = bisect_state_for_repo(
|
||||||
|
&repo.path,
|
||||||
|
String::from_utf8_lossy(&output).trim().to_string(),
|
||||||
|
)
|
||||||
|
.expect("bisect state should load");
|
||||||
|
assert!(state.active);
|
||||||
|
assert!(state.current.is_some());
|
||||||
|
|
||||||
|
for _ in 0..8 {
|
||||||
|
if state.finished {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let output =
|
||||||
|
run_git(&repo.path, ["bisect", "good"]).expect("bisect verdict should succeed");
|
||||||
|
state = bisect_state_for_repo(
|
||||||
|
&repo.path,
|
||||||
|
String::from_utf8_lossy(&output).trim().to_string(),
|
||||||
|
)
|
||||||
|
.expect("bisect state should advance");
|
||||||
|
}
|
||||||
|
assert!(state.finished);
|
||||||
|
assert_eq!(
|
||||||
|
state.culprit.as_ref().map(|commit| &commit.hash),
|
||||||
|
Some(&hashes[5])
|
||||||
|
);
|
||||||
|
|
||||||
|
run_git(&repo.path, ["bisect", "reset"]).expect("bisect should reset");
|
||||||
|
assert!(!bisect_in_progress(&repo.path).expect("bisect state should be readable"));
|
||||||
|
assert_eq!(
|
||||||
|
git_output_test(&repo.path, ["rev-parse", "HEAD"]),
|
||||||
|
hashes[5]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -17,20 +17,21 @@ use git::{
|
|||||||
commit_ai_review, commit_ai_split, compare_commits, compare_file_to_head,
|
commit_ai_review, commit_ai_split, compare_commits, compare_file_to_head,
|
||||||
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
|
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
|
||||||
delete_branch, delete_commit_note, delete_remote_branch, delete_remote_branches, delete_tag,
|
delete_branch, delete_commit_note, delete_remote_branch, delete_remote_branches, delete_tag,
|
||||||
diff_file_against_working_tree, fetch, fetch_commit_notes, get_commit_note, get_file_blame,
|
diff_file_against_working_tree, fetch, fetch_commit_notes, get_bisect_state, get_commit_note,
|
||||||
get_file_patch, get_remote_url, get_status, git_lfs_install, git_lfs_prune, git_lfs_pull,
|
get_file_blame, get_file_patch, get_remote_url, get_status, git_lfs_install, git_lfs_prune,
|
||||||
git_lfs_status, git_lfs_track, git_lfs_untrack, init_repository, last_commit_message,
|
git_lfs_pull, git_lfs_status, git_lfs_track, git_lfs_untrack, init_repository,
|
||||||
list_branches, list_commits, list_file_history, list_interactive_rebase_commits, list_reflog,
|
last_commit_message, list_branches, list_commits, list_file_history,
|
||||||
list_remotes, list_repository_files, list_stashes, list_tags, list_worktrees, lock_worktree,
|
list_interactive_rebase_commits, list_reflog, list_remotes, list_repository_files,
|
||||||
merge_abort, merge_branch, merge_continue, move_worktree, open_repo_in_explorer,
|
list_stashes, list_tags, list_worktrees, lock_worktree, mark_bisect, merge_abort, merge_branch,
|
||||||
open_repository, open_repository_bundle, open_repository_file, prune_worktrees, pull, push,
|
merge_continue, move_worktree, open_repo_in_explorer, open_repository, open_repository_bundle,
|
||||||
push_commit_notes, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue,
|
open_repository_file, prune_worktrees, pull, push, push_commit_notes, push_tag, read_conflict,
|
||||||
remove_remote, remove_worktree, rename_branch, rename_remote_branch, repair_worktree,
|
rebase_abort, rebase_branch, rebase_continue, remove_remote, remove_worktree, rename_branch,
|
||||||
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
rename_remote_branch, repair_worktree, reset_bisect, resolve_conflict, resolve_conflict_side,
|
||||||
restore_reflog_entry, restore_to_commit, revert_commit, run_sequence_editor_if_requested,
|
restore_file_from_commit, restore_files, restore_reflog_entry, restore_to_commit,
|
||||||
search_code_introductions, set_branch_upstream, set_commit_note, stage_files,
|
revert_commit, run_sequence_editor_if_requested, search_code_introductions,
|
||||||
start_interactive_rebase, stash_apply, stash_drop, stash_pop, stash_push, undo_last_commit,
|
set_branch_upstream, set_commit_note, stage_files, start_bisect, start_interactive_rebase,
|
||||||
unlock_worktree, unstage_files, untrack_paths, update_remote,
|
stash_apply, stash_drop, stash_pop, stash_push, undo_last_commit, unlock_worktree,
|
||||||
|
unstage_files, untrack_paths, update_remote,
|
||||||
};
|
};
|
||||||
use integrations::list_integration_repositories;
|
use integrations::list_integration_repositories;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -411,6 +412,10 @@ async fn main() {
|
|||||||
start_interactive_rebase,
|
start_interactive_rebase,
|
||||||
list_reflog,
|
list_reflog,
|
||||||
restore_reflog_entry,
|
restore_reflog_entry,
|
||||||
|
get_bisect_state,
|
||||||
|
start_bisect,
|
||||||
|
mark_bisect,
|
||||||
|
reset_bisect,
|
||||||
list_repository_files,
|
list_repository_files,
|
||||||
open_repository_bundle,
|
open_repository_bundle,
|
||||||
list_file_history,
|
list_file_history,
|
||||||
|
|||||||
+81
-1
@@ -68,6 +68,7 @@
|
|||||||
compareFileToParent,
|
compareFileToParent,
|
||||||
fetchCommitNotes,
|
fetchCommitNotes,
|
||||||
fetchRemote,
|
fetchRemote,
|
||||||
|
getBisectState,
|
||||||
getCommitNote,
|
getCommitNote,
|
||||||
getFileBlame,
|
getFileBlame,
|
||||||
getGitLfsStatus,
|
getGitLfsStatus,
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
listReflog,
|
listReflog,
|
||||||
listRepositoryFiles,
|
listRepositoryFiles,
|
||||||
mergeBranch,
|
mergeBranch,
|
||||||
|
markBisect,
|
||||||
mergeAbort,
|
mergeAbort,
|
||||||
mergeContinue,
|
mergeContinue,
|
||||||
openRepoInExplorer,
|
openRepoInExplorer,
|
||||||
@@ -102,6 +104,7 @@
|
|||||||
removeRemote,
|
removeRemote,
|
||||||
removeWorktree,
|
removeWorktree,
|
||||||
repairWorktree,
|
repairWorktree,
|
||||||
|
resetBisect,
|
||||||
revertCommit,
|
revertCommit,
|
||||||
setBranchUpstream,
|
setBranchUpstream,
|
||||||
setCommitNote,
|
setCommitNote,
|
||||||
@@ -128,6 +131,7 @@
|
|||||||
restoreFiles,
|
restoreFiles,
|
||||||
restoreToCommit,
|
restoreToCommit,
|
||||||
searchCodeIntroductions,
|
searchCodeIntroductions,
|
||||||
|
startBisect,
|
||||||
startInteractiveRebase,
|
startInteractiveRebase,
|
||||||
setSyncBadge,
|
setSyncBadge,
|
||||||
stageFiles,
|
stageFiles,
|
||||||
@@ -151,6 +155,7 @@
|
|||||||
AppLanguage,
|
AppLanguage,
|
||||||
AppTheme,
|
AppTheme,
|
||||||
AnalyticsSettings,
|
AnalyticsSettings,
|
||||||
|
BisectState,
|
||||||
CloneOptions,
|
CloneOptions,
|
||||||
CustomThemeColors,
|
CustomThemeColors,
|
||||||
ConflictFile,
|
ConflictFile,
|
||||||
@@ -423,6 +428,10 @@
|
|||||||
let reflogEntries: ReflogEntry[] = [];
|
let reflogEntries: ReflogEntry[] = [];
|
||||||
let reflogLoading = false;
|
let reflogLoading = false;
|
||||||
let reflogError = "";
|
let reflogError = "";
|
||||||
|
let bisectOpen = false;
|
||||||
|
let bisectState: BisectState | null = null;
|
||||||
|
let bisectLoading = false;
|
||||||
|
let bisectError = "";
|
||||||
let selectedDiffPath = "";
|
let selectedDiffPath = "";
|
||||||
let diffHighlightQuery = "";
|
let diffHighlightQuery = "";
|
||||||
let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
|
let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
|
||||||
@@ -999,7 +1008,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function autoRefreshTick() {
|
async function autoRefreshTick() {
|
||||||
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || gitLfsDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || bisectOpen || worktreeDialogOpen || gitLfsDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
||||||
const path = activeRepoPath;
|
const path = activeRepoPath;
|
||||||
autoRefreshInFlight = true;
|
autoRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
@@ -3325,6 +3334,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openBisect() {
|
||||||
|
if (!hasRepository || isBusy) return;
|
||||||
|
bisectOpen = true;
|
||||||
|
bisectState = null;
|
||||||
|
bisectError = "";
|
||||||
|
bisectLoading = true;
|
||||||
|
try {
|
||||||
|
bisectState = await getBisectState(activeRepoPath);
|
||||||
|
trackEvent("bisect_opened", { active: bisectState.active ? 1 : 0 });
|
||||||
|
} catch (error) {
|
||||||
|
bisectError = errorToMessage(error);
|
||||||
|
} finally {
|
||||||
|
bisectLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function beginBisect(good: string, bad: string) {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
|
bisectError = "";
|
||||||
|
await runOperation("Starting Git bisect", async () => {
|
||||||
|
bisectState = await startBisect(activeRepoPath, good, bad);
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
trackEvent("bisect_started");
|
||||||
|
});
|
||||||
|
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function continueBisect(verdict: "good" | "bad" | "skip") {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
|
bisectError = "";
|
||||||
|
await runOperation("Continuing Git bisect", async () => {
|
||||||
|
bisectState = await markBisect(activeRepoPath, verdict);
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
trackEvent("bisect_commit_marked", { verdict, finished: bisectState.finished ? 1 : 0 });
|
||||||
|
});
|
||||||
|
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopBisect() {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
|
bisectError = "";
|
||||||
|
await runOperation("Stopping Git bisect", async () => {
|
||||||
|
applyStatus(await resetBisect(activeRepoPath));
|
||||||
|
bisectOpen = false;
|
||||||
|
bisectState = null;
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
trackEvent("bisect_stopped");
|
||||||
|
});
|
||||||
|
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
async function previewReflogEntry(entry: ReflogEntry) {
|
async function previewReflogEntry(entry: ReflogEntry) {
|
||||||
if (!activeRepoPath || isBusy) return;
|
if (!activeRepoPath || isBusy) return;
|
||||||
await runOperation("Previewing reflog entry", async () => {
|
await runOperation("Previewing reflog entry", async () => {
|
||||||
@@ -5041,6 +5101,7 @@
|
|||||||
else if (event.key === "Escape" && gitLfsDialogOpen) closeGitLfsDialog();
|
else if (event.key === "Escape" && gitLfsDialogOpen) closeGitLfsDialog();
|
||||||
else if (event.key === "Escape" && interactiveRebaseOpen && !isBusy) interactiveRebaseOpen = false;
|
else if (event.key === "Escape" && interactiveRebaseOpen && !isBusy) interactiveRebaseOpen = false;
|
||||||
else if (event.key === "Escape" && reflogOpen && !isBusy) reflogOpen = false;
|
else if (event.key === "Escape" && reflogOpen && !isBusy) reflogOpen = false;
|
||||||
|
else if (event.key === "Escape" && bisectOpen && !isBusy) bisectOpen = false;
|
||||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||||
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
||||||
}
|
}
|
||||||
@@ -5106,6 +5167,7 @@
|
|||||||
onCompare={openCompareSelect}
|
onCompare={openCompareSelect}
|
||||||
onInteractiveRebase={openInteractiveRebase}
|
onInteractiveRebase={openInteractiveRebase}
|
||||||
onReflog={openReflog}
|
onReflog={openReflog}
|
||||||
|
onBisect={openBisect}
|
||||||
onOpenInEditor={openActiveRepoInEditor}
|
onOpenInEditor={openActiveRepoInEditor}
|
||||||
onOpenTerminal={openActiveRepoTerminal}
|
onOpenTerminal={openActiveRepoTerminal}
|
||||||
onOpenInExplorer={openActiveRepoFileManager}
|
onOpenInExplorer={openActiveRepoFileManager}
|
||||||
@@ -5579,6 +5641,7 @@
|
|||||||
onOpenSearch={openGlobalSearchDialog}
|
onOpenSearch={openGlobalSearchDialog}
|
||||||
onOpenCompare={openCompareSelect}
|
onOpenCompare={openCompareSelect}
|
||||||
onOpenReflog={openReflog}
|
onOpenReflog={openReflog}
|
||||||
|
onOpenBisect={openBisect}
|
||||||
onOpenInteractiveRebase={openInteractiveRebase}
|
onOpenInteractiveRebase={openInteractiveRebase}
|
||||||
onOpenWorktrees={openWorktreeDialog}
|
onOpenWorktrees={openWorktreeDialog}
|
||||||
onOpenSyncSettings={openSyncOptions}
|
onOpenSyncSettings={openSyncOptions}
|
||||||
@@ -5879,6 +5942,23 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if bisectOpen}
|
||||||
|
{#await import("./lib/components/BisectDialog.svelte") then module}
|
||||||
|
<module.default
|
||||||
|
language={appLanguage}
|
||||||
|
{bisectState}
|
||||||
|
isLoading={bisectLoading}
|
||||||
|
{isBusy}
|
||||||
|
{operation}
|
||||||
|
error={bisectError}
|
||||||
|
onStart={beginBisect}
|
||||||
|
onMark={continueBisect}
|
||||||
|
onReset={stopBisect}
|
||||||
|
onClose={() => { if (!isBusy) bisectOpen = false; }}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Compare: pick two branches or commits to diff -->
|
<!-- Compare: pick two branches or commits to diff -->
|
||||||
{#if compareSelectOpen}
|
{#if compareSelectOpen}
|
||||||
<CompareSelectDialog
|
<CompareSelectDialog
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Bug,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Code2,
|
Code2,
|
||||||
CloudDownload,
|
CloudDownload,
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
export let onCompare: () => void = () => {};
|
export let onCompare: () => void = () => {};
|
||||||
export let onInteractiveRebase: () => void = () => {};
|
export let onInteractiveRebase: () => void = () => {};
|
||||||
export let onReflog: () => void = () => {};
|
export let onReflog: () => void = () => {};
|
||||||
|
export let onBisect: () => void = () => {};
|
||||||
export let onOpenInExplorer: () => void = () => {};
|
export let onOpenInExplorer: () => void = () => {};
|
||||||
export let onOpenInEditor: () => void = () => {};
|
export let onOpenInEditor: () => void = () => {};
|
||||||
export let onOpenTerminal: () => void = () => {};
|
export let onOpenTerminal: () => void = () => {};
|
||||||
@@ -208,6 +210,13 @@
|
|||||||
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
|
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" role="menuitem" onclick={() => runHistoryAction(onBisect)}>
|
||||||
|
<Bug size={15} aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
<strong>Git Bisect</strong>
|
||||||
|
<small>{isGerman ? "Fehlerhaften Commit schrittweise finden" : "Find a bad commit step by step"}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertTriangle, Bug, Check, GitCommitHorizontal, LoaderCircle, Play, RotateCcw, SkipForward, X } from "@lucide/svelte";
|
||||||
|
import type { BisectState } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
language: "en" | "de";
|
||||||
|
bisectState: BisectState | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isBusy: boolean;
|
||||||
|
operation: string;
|
||||||
|
error: string;
|
||||||
|
onStart: (good: string, bad: string) => void;
|
||||||
|
onMark: (verdict: "good" | "bad" | "skip") => void;
|
||||||
|
onReset: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { language = "en", bisectState = null, isLoading = false, isBusy = false, operation = "", error = "", onStart = () => {}, onMark = () => {}, onReset = () => {}, onClose = () => {} }: Props = $props();
|
||||||
|
let good = $state("");
|
||||||
|
let bad = $state("HEAD");
|
||||||
|
const de = $derived(language === "de");
|
||||||
|
const canStart = $derived(Boolean(good.trim() && bad.trim()) && !isLoading && !isBusy);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||||||
|
<div class="dialog bisect-dialog" role="dialog" aria-modal="true" aria-label={de ? "Geführtes Git Bisect" : "Guided Git bisect"} tabindex="-1">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div><span class="eyebrow">{de ? "Fehlerursache eingrenzen" : "Find the regression"}</span><h2 class="dialog-title">Git Bisect</h2></div>
|
||||||
|
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title={de ? "Schließen" : "Close"}><X size={18} aria-hidden="true" /></button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bisect-body">
|
||||||
|
{#if error}<div class="bisect-notice error"><AlertTriangle size={16} /><span>{error}</span></div>{/if}
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="bisect-loading"><LoaderCircle class="spin" size={18} />{de ? "Bisect-Status wird geladen …" : "Loading bisect status …"}</div>
|
||||||
|
{:else if !bisectState?.active}
|
||||||
|
<section class="bisect-intro">
|
||||||
|
<div class="bisect-intro-icon"><Bug size={24} /></div>
|
||||||
|
<div><h3>{de ? "Welcher Commit hat den Fehler eingeführt?" : "Which commit introduced the bug?"}</h3><p>{de ? "Gitty checkt schrittweise frühere Commits aus. Markiere jede getestete Version als Good, Bad oder Skip." : "Gitty checks out earlier commits step by step. Mark each tested version as Good, Bad, or Skip."}</p></div>
|
||||||
|
</section>
|
||||||
|
<div class="bisect-range">
|
||||||
|
<div class="range-heading">
|
||||||
|
<span>{de ? "Suchbereich" : "Search range"}</span>
|
||||||
|
<small>{de ? "Von funktionierend bis fehlerhaft" : "From working to broken"}</small>
|
||||||
|
</div>
|
||||||
|
<div class="bisect-fields">
|
||||||
|
<label class="commit-field good-field">
|
||||||
|
<span class="field-title"><Check size={14} />{de ? "Funktionierender Commit" : "Known good commit"}<em>Good</em></span>
|
||||||
|
<input bind:value={good} placeholder={de ? "z. B. v1.4.0 oder Commit-Hash" : "e.g. v1.4.0 or commit hash"} disabled={isBusy} spellcheck="false" autocomplete="off" />
|
||||||
|
<small>{de ? "Hier trat der Fehler noch nicht auf." : "The bug did not occur here."}</small>
|
||||||
|
</label>
|
||||||
|
<label class="commit-field bad-field">
|
||||||
|
<span class="field-title"><Bug size={14} />{de ? "Fehlerhafter Commit" : "Known bad commit"}<em>Bad</em></span>
|
||||||
|
<input bind:value={bad} placeholder="HEAD" disabled={isBusy} spellcheck="false" autocomplete="off" />
|
||||||
|
<small>{de ? "Meistens HEAD, der aktuelle Commit." : "Usually HEAD, the current commit."}</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bisect-notice"><AlertTriangle size={15} /><span>{de ? "Der Arbeitsbaum muss sauber sein. Während des Bisects wechselt Gitty vorübergehend zwischen Commits." : "The working tree must be clean. Gitty temporarily switches between commits during the bisect."}</span></div>
|
||||||
|
{:else if bisectState.finished && bisectState.culprit}
|
||||||
|
<section class="bisect-result">
|
||||||
|
<div class="result-icon"><Bug size={22} /></div>
|
||||||
|
<div><span>{de ? "Erster fehlerhafter Commit gefunden" : "First bad commit found"}</span><h3>{bisectState.culprit.summary}</h3><div class="commit-meta"><code>{bisectState.culprit.short_hash}</code><span>{bisectState.culprit.author_name}</span><span>{new Date(bisectState.culprit.date).toLocaleString()}</span></div></div>
|
||||||
|
</section>
|
||||||
|
<p class="result-copy">{de ? "Beende den Bisect, um zum ursprünglichen Branch und Arbeitsstand zurückzukehren." : "Finish the bisect to return to the original branch and working state."}</p>
|
||||||
|
{:else if bisectState.current}
|
||||||
|
<div class="bisect-progress">
|
||||||
|
<span>{de ? "Aktueller Test-Commit" : "Current test commit"}</span>
|
||||||
|
{#if bisectState.remaining_steps !== null}<strong>{de ? `Noch ungefähr ${bisectState.remaining_steps} Schritte` : `About ${bisectState.remaining_steps} steps remaining`}</strong>{/if}
|
||||||
|
</div>
|
||||||
|
<section class="bisect-current">
|
||||||
|
<GitCommitHorizontal size={22} />
|
||||||
|
<div><h3>{bisectState.current.summary}</h3><div class="commit-meta"><code>{bisectState.current.short_hash}</code><span>{bisectState.current.author_name}</span><span>{new Date(bisectState.current.date).toLocaleString()}</span></div></div>
|
||||||
|
</section>
|
||||||
|
<div class="bisect-question"><strong>{de ? "Tritt der Fehler bei diesem Commit auf?" : "Does the bug occur at this commit?"}</strong><span>{de ? "Teste die Anwendung oder führe deine Prüfschritte aus, bevor du entscheidest." : "Test the application or run your checks before choosing a verdict."}</span></div>
|
||||||
|
<div class="verdict-grid">
|
||||||
|
<button class="verdict good" type="button" onclick={() => onMark("good")} disabled={isBusy}><Check size={17} /><span><strong>Good</strong><small>{de ? "Fehler tritt nicht auf" : "Bug is absent"}</small></span></button>
|
||||||
|
<button class="verdict bad" type="button" onclick={() => onMark("bad")} disabled={isBusy}><Bug size={17} /><span><strong>Bad</strong><small>{de ? "Fehler tritt auf" : "Bug occurs"}</small></span></button>
|
||||||
|
<button class="verdict skip" type="button" onclick={() => onMark("skip")} disabled={isBusy}><SkipForward size={17} /><span><strong>Skip</strong><small>{de ? "Nicht testbar" : "Cannot test"}</small></span></button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="dialog-footer bisect-footer">
|
||||||
|
{#if bisectState?.active}
|
||||||
|
<button class="btn-secondary reset" type="button" onclick={onReset} disabled={isBusy}><RotateCcw size={15} />{bisectState.finished ? (de ? "Bisect beenden" : "Finish bisect") : (de ? "Bisect abbrechen" : "Abort bisect")}</button>
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>{de ? "Später fortsetzen" : "Continue later"}</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button>
|
||||||
|
<button class="btn-primary" type="button" onclick={() => onStart(good.trim(), bad.trim())} disabled={!canStart}>{#if operation === "Starting Git bisect"}<LoaderCircle class="spin" size={16} />{:else}<Play size={16} />{/if}{de ? "Bisect starten" : "Start bisect"}</button>
|
||||||
|
{/if}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bisect-dialog{grid-template-rows:auto minmax(0,auto) auto;width:min(640px,calc(100vw - 28px));height:auto;max-height:calc(100vh - 40px);overflow:hidden}
|
||||||
|
.bisect-body{display:grid;align-content:start;gap:14px;padding:16px 18px;overflow:auto}
|
||||||
|
.bisect-loading{display:flex;min-height:150px;align-items:center;justify-content:center;gap:8px;color:var(--color-ink-muted)}
|
||||||
|
.bisect-intro,.bisect-current,.bisect-result{display:flex;align-items:flex-start;gap:12px}
|
||||||
|
.bisect-intro-icon,.result-icon{display:grid;width:38px;height:38px;flex:0 0 38px;place-items:center;border:1px solid color-mix(in srgb,var(--color-primary) 32%,var(--color-border));border-radius:8px;color:var(--color-primary);background:color-mix(in srgb,var(--color-primary) 9%,var(--color-surface))}
|
||||||
|
.bisect-intro h3,.bisect-current h3,.bisect-result h3{margin:0;font-size:13px}
|
||||||
|
.bisect-intro p,.result-copy{max-width:520px;margin:4px 0 0;color:var(--color-ink-muted);font-size:12px;line-height:1.45}
|
||||||
|
.bisect-range{display:grid;gap:9px;padding:12px;border:1px solid var(--color-border-subtle);border-radius:9px;background:color-mix(in srgb,var(--color-surface) 72%,transparent)}
|
||||||
|
.range-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
|
||||||
|
.range-heading>span{color:var(--color-ink);font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase}
|
||||||
|
.range-heading>small{color:var(--color-ink-faint);font-size:10px}
|
||||||
|
.bisect-fields{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||||
|
.commit-field{display:grid;gap:6px;padding:10px;border:1px solid var(--color-border-subtle);border-radius:7px;background:var(--color-surface-raised)}
|
||||||
|
.field-title{display:flex;align-items:center;gap:6px;color:var(--color-ink);font-size:11px;font-weight:700}
|
||||||
|
.field-title em{margin-left:auto;padding:1px 5px;border-radius:4px;font-size:9px;font-style:normal;font-weight:800;text-transform:uppercase}
|
||||||
|
.good-field .field-title :global(svg),.good-field .field-title em{color:#69c986}
|
||||||
|
.good-field .field-title em{background:color-mix(in srgb,#69c986 10%,transparent)}
|
||||||
|
.bad-field .field-title :global(svg),.bad-field .field-title em{color:#ef737b}
|
||||||
|
.bad-field .field-title em{background:color-mix(in srgb,#ef737b 10%,transparent)}
|
||||||
|
.bisect-fields input{height:32px;font-family:var(--font-mono);font-size:11.5px}
|
||||||
|
.bisect-fields small{color:var(--color-ink-faint);font-size:9.5px;line-height:1.35}
|
||||||
|
.bisect-notice{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border:1px solid color-mix(in srgb,#e2ad4e 28%,var(--color-border));border-radius:7px;color:var(--color-ink-muted);background:color-mix(in srgb,#e2ad4e 6%,var(--color-surface));font-size:11.5px;line-height:1.4}
|
||||||
|
.bisect-notice :global(svg){flex:0 0 auto;color:#e2ad4e}
|
||||||
|
.bisect-notice.error{border-color:color-mix(in srgb,#e45c65 40%,var(--color-border));color:#ef979d;background:color-mix(in srgb,#c92f3a 8%,var(--color-surface))}
|
||||||
|
.bisect-notice.error :global(svg){color:#ef6972}
|
||||||
|
.bisect-progress{display:flex;align-items:center;justify-content:space-between;color:var(--color-ink-muted);font-size:11px}
|
||||||
|
.bisect-progress strong{color:var(--color-primary)}
|
||||||
|
.bisect-current{padding:14px;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}
|
||||||
|
.bisect-current>:global(svg){flex:0 0 auto;color:var(--color-primary)}
|
||||||
|
.commit-meta{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;margin-top:7px;color:var(--color-ink-faint);font-size:10px}
|
||||||
|
.commit-meta code{padding:2px 5px;border-radius:4px;color:var(--color-primary);background:color-mix(in srgb,var(--color-primary) 10%,transparent)}
|
||||||
|
.bisect-question{display:grid;gap:4px}
|
||||||
|
.bisect-question span{color:var(--color-ink-muted);font-size:11px}
|
||||||
|
.verdict-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
|
||||||
|
.verdict{min-height:58px;justify-content:flex-start;padding:8px 11px;text-align:left}
|
||||||
|
.verdict>span{display:grid;gap:2px}
|
||||||
|
.verdict small{color:var(--color-ink-faint);font-size:9.5px}
|
||||||
|
.verdict.good{color:#69c986}.verdict.bad{color:#ef737b}.verdict.skip{color:#ddb45c}
|
||||||
|
.bisect-result{padding:16px;border:1px solid color-mix(in srgb,#ef737b 36%,var(--color-border));border-radius:8px;background:color-mix(in srgb,#c92f3a 7%,var(--color-surface))}
|
||||||
|
.bisect-result>div:last-child>span{color:#ef838b;font-size:10px;font-weight:800;text-transform:uppercase}
|
||||||
|
.result-copy{margin:0}
|
||||||
|
.bisect-footer{justify-content:flex-end;padding-block:9px}
|
||||||
|
.bisect-footer .reset{margin-right:auto}
|
||||||
|
@media(max-width:620px){.bisect-fields,.verdict-grid{grid-template-columns:1fr}.bisect-body{padding:14px}.range-heading{align-items:flex-start;flex-direction:column;gap:2px}.bisect-progress{align-items:flex-start;flex-direction:column;gap:4px}}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import {
|
import {
|
||||||
ArrowDownToLine, ArrowUpFromLine, Boxes, CircleHelp, FileCode, GitBranch,
|
ArrowDownToLine, ArrowUpFromLine, Boxes, Bug, CircleHelp, FileCode, GitBranch,
|
||||||
GitCompare, History, RefreshCw, Search, Settings, SlidersHorizontal, Sparkles,
|
GitCompare, History, RefreshCw, Search, Settings, SlidersHorizontal, Sparkles,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import type { AppLanguage, GitBranch as GitBranchInfo, GitCommit, GitRepositoryFile } from "../types";
|
import type { AppLanguage, GitBranch as GitBranchInfo, GitCommit, GitRepositoryFile } from "../types";
|
||||||
|
|
||||||
type ItemKind = "fetch" | "pull" | "push" | "refresh" | "search" | "compare" | "reflog" | "rebase" | "worktrees" | "sync" | "settings" | "ai-settings" | "help" | "branch" | "file" | "commit";
|
type ItemKind = "fetch" | "pull" | "push" | "refresh" | "search" | "compare" | "reflog" | "rebase" | "bisect" | "worktrees" | "sync" | "settings" | "ai-settings" | "help" | "branch" | "file" | "commit";
|
||||||
interface Item { id: string; group: string; kind: ItemKind; title: string; subtitle: string; search: string; disabled?: boolean; run: () => void | Promise<void>; }
|
interface Item { id: string; group: string; kind: ItemKind; title: string; subtitle: string; search: string; disabled?: boolean; run: () => void | Promise<void>; }
|
||||||
interface Props {
|
interface Props {
|
||||||
language: AppLanguage; hasRepository: boolean; isBusy: boolean;
|
language: AppLanguage; hasRepository: boolean; isBusy: boolean;
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
onSelectCommit: (commit: GitCommit) => void | Promise<void>;
|
onSelectCommit: (commit: GitCommit) => void | Promise<void>;
|
||||||
onFetch: () => void | Promise<void>; onPull: () => void | Promise<void>; onPush: () => void | Promise<void>;
|
onFetch: () => void | Promise<void>; onPull: () => void | Promise<void>; onPush: () => void | Promise<void>;
|
||||||
onRefresh: () => void | Promise<void>; onOpenSearch: () => void; onOpenCompare: () => void;
|
onRefresh: () => void | Promise<void>; onOpenSearch: () => void; onOpenCompare: () => void;
|
||||||
onOpenReflog: () => void | Promise<void>; onOpenInteractiveRebase: () => void;
|
onOpenReflog: () => void | Promise<void>; onOpenInteractiveRebase: () => void; onOpenBisect: () => void | Promise<void>;
|
||||||
onOpenWorktrees: () => void | Promise<void>; onOpenSyncSettings: () => void | Promise<void>;
|
onOpenWorktrees: () => void | Promise<void>; onOpenSyncSettings: () => void | Promise<void>;
|
||||||
onOpenSettings: () => void; onOpenAiSettings: () => void; onOpenHelp: () => void;
|
onOpenSettings: () => void; onOpenAiSettings: () => void; onOpenHelp: () => void;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
language = "en", hasRepository = false, isBusy = false, branches = [], files = [], commits = [], onClose = () => {},
|
language = "en", hasRepository = false, isBusy = false, branches = [], files = [], commits = [], onClose = () => {},
|
||||||
onCheckoutBranch = () => {}, onOpenFile = () => {}, onSelectCommit = () => {}, onFetch = () => {}, onPull = () => {},
|
onCheckoutBranch = () => {}, onOpenFile = () => {}, onSelectCommit = () => {}, onFetch = () => {}, onPull = () => {},
|
||||||
onPush = () => {}, onRefresh = () => {}, onOpenSearch = () => {}, onOpenCompare = () => {}, onOpenReflog = () => {},
|
onPush = () => {}, onRefresh = () => {}, onOpenSearch = () => {}, onOpenCompare = () => {}, onOpenReflog = () => {},
|
||||||
onOpenInteractiveRebase = () => {}, onOpenWorktrees = () => {}, onOpenSyncSettings = () => {}, onOpenSettings = () => {},
|
onOpenInteractiveRebase = () => {}, onOpenBisect = () => {}, onOpenWorktrees = () => {}, onOpenSyncSettings = () => {}, onOpenSettings = () => {},
|
||||||
onOpenAiSettings = () => {}, onOpenHelp = () => {},
|
onOpenAiSettings = () => {}, onOpenHelp = () => {},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
action("compare", "compare", isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits", isGerman ? "Zwei vollständige Revisionen vergleichen" : "Diff two complete revisions", onOpenCompare),
|
action("compare", "compare", isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits", isGerman ? "Zwei vollständige Revisionen vergleichen" : "Diff two complete revisions", onOpenCompare),
|
||||||
action("reflog", "reflog", "Reflog", isGerman ? "Verlorene Commits finden und wiederherstellen" : "Find and recover lost commits", onOpenReflog),
|
action("reflog", "reflog", "Reflog", isGerman ? "Verlorene Commits finden und wiederherstellen" : "Find and recover lost commits", onOpenReflog),
|
||||||
action("rebase", "rebase", "Interactive Rebase", isGerman ? "Commit-Historie bearbeiten" : "Edit commit history", onOpenInteractiveRebase),
|
action("rebase", "rebase", "Interactive Rebase", isGerman ? "Commit-Historie bearbeiten" : "Edit commit history", onOpenInteractiveRebase),
|
||||||
|
action("bisect", "bisect", "Git Bisect", isGerman ? "Fehlerhaften Commit schrittweise finden" : "Find a bad commit step by step", onOpenBisect),
|
||||||
action("worktrees", "worktrees", "Worktrees", isGerman ? "Arbeitsverzeichnisse verwalten" : "Manage linked working trees", onOpenWorktrees),
|
action("worktrees", "worktrees", "Worktrees", isGerman ? "Arbeitsverzeichnisse verwalten" : "Manage linked working trees", onOpenWorktrees),
|
||||||
action("sync", "sync", isGerman ? "Synchronisierung konfigurieren" : "Configure synchronization", isGerman ? "Remote, Upstream und Pull-Strategie" : "Remote, upstream and pull strategy", onOpenSyncSettings),
|
action("sync", "sync", isGerman ? "Synchronisierung konfigurieren" : "Configure synchronization", isGerman ? "Remote, Upstream und Pull-Strategie" : "Remote, upstream and pull strategy", onOpenSyncSettings),
|
||||||
action("settings", "settings", isGerman ? "Einstellungen" : "Settings", isGerman ? "Darstellung, Sprache und Verhalten" : "Appearance, language and behavior", onOpenSettings, false),
|
action("settings", "settings", isGerman ? "Einstellungen" : "Settings", isGerman ? "Darstellung, Sprache und Verhalten" : "Appearance, language and behavior", onOpenSettings, false),
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
{:else if item.kind === "compare"}<GitCompare size={16} />
|
{:else if item.kind === "compare"}<GitCompare size={16} />
|
||||||
{:else if item.kind === "reflog" || item.kind === "commit"}<History size={16} />
|
{:else if item.kind === "reflog" || item.kind === "commit"}<History size={16} />
|
||||||
{:else if item.kind === "rebase" || item.kind === "branch"}<GitBranch size={16} />
|
{:else if item.kind === "rebase" || item.kind === "branch"}<GitBranch size={16} />
|
||||||
|
{:else if item.kind === "bisect"}<Bug size={16} />
|
||||||
{:else if item.kind === "worktrees"}<Boxes size={16} />
|
{:else if item.kind === "worktrees"}<Boxes size={16} />
|
||||||
{:else if item.kind === "sync"}<SlidersHorizontal size={16} />
|
{:else if item.kind === "sync"}<SlidersHorizontal size={16} />
|
||||||
{:else if item.kind === "settings"}<Settings size={16} />
|
{:else if item.kind === "settings"}<Settings size={16} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { tracedInvoke as invoke } from "./telemetry";
|
|||||||
import type {
|
import type {
|
||||||
AiReviewResult,
|
AiReviewResult,
|
||||||
AiCommitPlan,
|
AiCommitPlan,
|
||||||
|
BisectState,
|
||||||
CommitAiProvider,
|
CommitAiProvider,
|
||||||
CloneOptions,
|
CloneOptions,
|
||||||
ConflictFile,
|
ConflictFile,
|
||||||
@@ -104,6 +105,22 @@ export function getStatus(path: string): Promise<GitStatus> {
|
|||||||
return invoke<GitStatus>("get_status", { path });
|
return invoke<GitStatus>("get_status", { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBisectState(path: string): Promise<BisectState> {
|
||||||
|
return invoke<BisectState>("get_bisect_state", { path });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBisect(path: string, good: string, bad: string): Promise<BisectState> {
|
||||||
|
return invoke<BisectState>("start_bisect", { path, good, bad });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markBisect(path: string, verdict: "good" | "bad" | "skip"): Promise<BisectState> {
|
||||||
|
return invoke<BisectState>("mark_bisect", { path, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetBisect(path: string): Promise<GitStatus> {
|
||||||
|
return invoke<GitStatus>("reset_bisect", { path });
|
||||||
|
}
|
||||||
|
|
||||||
export function getGitLfsStatus(path: string): Promise<GitLfsStatus> {
|
export function getGitLfsStatus(path: string): Promise<GitLfsStatus> {
|
||||||
return invoke<GitLfsStatus>("git_lfs_status", { path });
|
return invoke<GitLfsStatus>("git_lfs_status", { path });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,24 @@ export interface GitCommit {
|
|||||||
has_note: boolean;
|
has_note: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BisectCommit {
|
||||||
|
hash: string;
|
||||||
|
short_hash: string;
|
||||||
|
summary: string;
|
||||||
|
author_name: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BisectState {
|
||||||
|
active: boolean;
|
||||||
|
finished: boolean;
|
||||||
|
current: BisectCommit | null;
|
||||||
|
culprit: BisectCommit | null;
|
||||||
|
remaining_revisions: number | null;
|
||||||
|
remaining_steps: number | null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitCommitFile {
|
export interface GitCommitFile {
|
||||||
path: string;
|
path: string;
|
||||||
old_path: string | null;
|
old_path: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user