feat(git): convert Git commands to async for better performance
This update modifies several Git command functions to be asynchronous, improving the responsiveness of the application. By utilizing async runtime, operations that involve I/O or long-running tasks can now run without blocking the main thread, enhancing user experience. - Converted multiple Git command functions to async - Introduced a helper function to handle async tasks with error management - Improved overall performance and responsiveness of Git operations
This commit is contained in:
+347
-155
@@ -382,13 +382,13 @@ fn check_search_cancelled(cancellation: Option<&SearchCancellation>) -> Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repository(path: String) -> Result<GitStatus, String> {
|
pub fn open_repository(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<GitStatus, String> {
|
pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<GitStatus, String> {
|
||||||
let path = PathBuf::from(path.trim());
|
let path = PathBuf::from(path.trim());
|
||||||
if path.as_os_str().is_empty() {
|
if path.as_os_str().is_empty() {
|
||||||
@@ -416,13 +416,13 @@ pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<G
|
|||||||
status_for_repo(&path)
|
status_for_repo(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repo_in_explorer(path: String) -> Result<(), String> {
|
pub fn open_repo_in_explorer(path: String) -> Result<(), String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
open_path_in_file_manager(&repo)
|
open_path_in_file_manager(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -448,6 +448,56 @@ pub struct RepositoryBundle {
|
|||||||
pub files: Vec<GitRepositoryFile>,
|
pub files: Vec<GitRepositoryFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_git_task<T, F>(context: &'static str, task: F) -> Result<T, String>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
F: FnOnce() -> Result<T, String> + Send + 'static,
|
||||||
|
{
|
||||||
|
tauri::async_runtime::spawn_blocking(task)
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("{context}: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_git_worker<T>(name: &str, result: thread::Result<Result<T, String>>) -> Result<T, String> {
|
||||||
|
result.map_err(|_| format!("The {name} Git worker stopped unexpectedly."))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository_bundle_for_repo(
|
||||||
|
repo: &Path,
|
||||||
|
commit_limit: Option<u32>,
|
||||||
|
) -> Result<RepositoryBundle, String> {
|
||||||
|
// Status is needed by the file tree. Once it is available, all remaining
|
||||||
|
// reads are independent and can run concurrently. Each worker only starts
|
||||||
|
// read-only Git processes, so this is safe while cutting the former
|
||||||
|
// branches -> tags -> stashes -> commits -> files waterfall down to the
|
||||||
|
// duration of its slowest member.
|
||||||
|
let status = status_for_repo(repo)?;
|
||||||
|
let (branches, tags, stashes, commits, files) = thread::scope(|scope| -> Result<_, String> {
|
||||||
|
let branches = scope.spawn(|| branches_for_repo(repo));
|
||||||
|
let tags = scope.spawn(|| tags_for_repo(repo));
|
||||||
|
let stashes = scope.spawn(|| stashes_for_repo(repo));
|
||||||
|
let commits = scope.spawn(|| commits_for_repo(repo, commit_limit));
|
||||||
|
let files = scope.spawn(|| repository_files_with_status(repo, &status));
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
join_git_worker("branch", branches.join())?,
|
||||||
|
join_git_worker("tag", tags.join())?,
|
||||||
|
join_git_worker("stash", stashes.join())?,
|
||||||
|
join_git_worker("history", commits.join())?,
|
||||||
|
join_git_worker("file tree", files.join())?,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(RepositoryBundle {
|
||||||
|
status,
|
||||||
|
branches,
|
||||||
|
tags,
|
||||||
|
stashes,
|
||||||
|
commits,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn clone_repository(
|
pub async fn clone_repository(
|
||||||
remote_url: String,
|
remote_url: String,
|
||||||
@@ -481,40 +531,32 @@ pub async fn open_repository_bundle(
|
|||||||
path: String,
|
path: String,
|
||||||
commit_limit: Option<u32>,
|
commit_limit: Option<u32>,
|
||||||
) -> Result<RepositoryBundle, String> {
|
) -> Result<RepositoryBundle, String> {
|
||||||
tauri::async_runtime::spawn_blocking(move || -> Result<RepositoryBundle, String> {
|
run_git_task("Could not load repository", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let status = status_for_repo(&repo)?;
|
repository_bundle_for_repo(&repo, commit_limit)
|
||||||
let branches = branches_for_repo(&repo)?;
|
|
||||||
let tags = tags_for_repo(&repo)?;
|
|
||||||
let stashes = stashes_for_repo(&repo)?;
|
|
||||||
let commits = commits_for_repo(&repo, commit_limit)?;
|
|
||||||
let files = repository_files_with_status(&repo, &status)?;
|
|
||||||
Ok(RepositoryBundle {
|
|
||||||
status,
|
|
||||||
branches,
|
|
||||||
tags,
|
|
||||||
stashes,
|
|
||||||
commits,
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Could not load repository: {err}"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_status(path: String) -> Result<GitStatus, String> {
|
pub async fn get_status(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not refresh repository status", move || {
|
||||||
status_for_repo(&repo)
|
let repo = resolve_repo(&path)?;
|
||||||
|
status_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
|
pub async fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not load branches", move || {
|
||||||
branches_for_repo(&repo)
|
let repo = resolve_repo(&path)?;
|
||||||
|
branches_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
||||||
log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}");
|
log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}");
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
@@ -535,7 +577,7 @@ pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_remote_name(&repo, &name, false)?;
|
let name = validate_remote_name(&repo, &name, false)?;
|
||||||
@@ -544,7 +586,7 @@ pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemo
|
|||||||
list_remotes(path)
|
list_remotes(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_remote_name(&repo, &name, true)?;
|
let name = validate_remote_name(&repo, &name, true)?;
|
||||||
@@ -553,7 +595,7 @@ pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitR
|
|||||||
list_remotes(path)
|
list_remotes(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> {
|
pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> {
|
||||||
log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
|
log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
|
||||||
let result = (|| {
|
let result = (|| {
|
||||||
@@ -574,7 +616,7 @@ pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, Strin
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn set_branch_upstream(
|
pub fn set_branch_upstream(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
@@ -607,7 +649,7 @@ pub fn set_branch_upstream(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn delete_remote_branch(
|
pub fn delete_remote_branch(
|
||||||
path: String,
|
path: String,
|
||||||
remote: String,
|
remote: String,
|
||||||
@@ -634,15 +676,21 @@ pub fn delete_remote_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_stashes(path: String) -> Result<Vec<GitStash>, String> {
|
pub async fn list_stashes(path: String) -> Result<Vec<GitStash>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not load stashes", move || {
|
||||||
stashes_for_repo(&repo)
|
let repo = resolve_repo(&path)?;
|
||||||
|
stashes_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_tags(path: String) -> Result<Vec<GitTag>, String> {
|
pub async fn list_tags(path: String) -> Result<Vec<GitTag>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not load tags", move || {
|
||||||
tags_for_repo(&repo)
|
let repo = resolve_repo(&path)?;
|
||||||
|
tags_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags_for_repo(repo: &Path) -> Result<Vec<GitTag>, String> {
|
fn tags_for_repo(repo: &Path) -> Result<Vec<GitTag>, String> {
|
||||||
@@ -790,39 +838,51 @@ fn parse_stash_subject(subject: &str) -> (Option<String>, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_push(
|
pub async fn stash_push(
|
||||||
path: String,
|
path: String,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
include_untracked: bool,
|
include_untracked: bool,
|
||||||
) -> Result<GitStatus, String> {
|
) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not stash changes", move || {
|
||||||
let trimmed_message = message.unwrap_or_default().trim().to_string();
|
let repo = resolve_repo(&path)?;
|
||||||
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
|
let trimmed_message = message.unwrap_or_default().trim().to_string();
|
||||||
if include_untracked {
|
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
|
||||||
args.push(OsString::from("--include-untracked"));
|
if include_untracked {
|
||||||
}
|
args.push(OsString::from("--include-untracked"));
|
||||||
if !trimmed_message.is_empty() {
|
}
|
||||||
args.push(OsString::from("-m"));
|
if !trimmed_message.is_empty() {
|
||||||
args.push(OsString::from(trimmed_message));
|
args.push(OsString::from("-m"));
|
||||||
}
|
args.push(OsString::from(trimmed_message));
|
||||||
|
}
|
||||||
|
|
||||||
run_git(&repo, args)?;
|
run_git(&repo, args)?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_apply(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_apply(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
run_stash_update(path, "apply", selector)
|
run_git_task("Could not apply stash", move || {
|
||||||
|
run_stash_update(path, "apply", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_pop(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_pop(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
run_stash_update(path, "pop", selector)
|
run_git_task("Could not pop stash", move || {
|
||||||
|
run_stash_update(path, "pop", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_drop(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_drop(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
run_stash_update(path, "drop", selector)
|
run_git_task("Could not drop stash", move || {
|
||||||
|
run_stash_update(path, "drop", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_stash_update(path: String, action: &str, selector: String) -> Result<GitStatus, String> {
|
fn run_stash_update(path: String, action: &str, selector: String) -> Result<GitStatus, String> {
|
||||||
@@ -859,7 +919,14 @@ fn validate_stash_selector(selector: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
pub async fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not check out branch", move || {
|
||||||
|
checkout_branch_core(path, branch)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkout_branch_core(path: String, branch: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let branch = branch.trim().to_string();
|
let branch = branch.trim().to_string();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
@@ -885,7 +952,18 @@ pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_branch(
|
pub async fn create_branch(
|
||||||
|
path: String,
|
||||||
|
branch: String,
|
||||||
|
start_point: Option<String>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not create branch", move || {
|
||||||
|
create_branch_core(path, branch, start_point)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
start_point: Option<String>,
|
start_point: Option<String>,
|
||||||
@@ -906,7 +984,18 @@ pub fn create_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn rename_branch(
|
pub async fn rename_branch(
|
||||||
|
path: String,
|
||||||
|
old_branch: String,
|
||||||
|
new_branch: String,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not rename branch", move || {
|
||||||
|
rename_branch_core(path, old_branch, new_branch)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
old_branch: String,
|
old_branch: String,
|
||||||
new_branch: String,
|
new_branch: String,
|
||||||
@@ -929,7 +1018,18 @@ pub fn rename_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_branch(
|
pub async fn delete_branch(
|
||||||
|
path: String,
|
||||||
|
branch: String,
|
||||||
|
force: Option<bool>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not delete branch", move || {
|
||||||
|
delete_branch_core(path, branch, force)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
force: Option<bool>,
|
force: Option<bool>,
|
||||||
@@ -1070,13 +1170,43 @@ fn worktrees_for_repo(repo: &Path) -> Result<Vec<GitWorktree>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn list_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not load worktrees", move || {
|
||||||
|
list_worktrees_core(path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_worktrees_core(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_worktree(
|
pub async fn add_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
branch: Option<String>,
|
||||||
|
new_branch: Option<String>,
|
||||||
|
start_point: Option<String>,
|
||||||
|
detached: Option<bool>,
|
||||||
|
lock: Option<bool>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not add worktree", move || {
|
||||||
|
add_worktree_core(
|
||||||
|
path,
|
||||||
|
worktree_path,
|
||||||
|
branch,
|
||||||
|
new_branch,
|
||||||
|
start_point,
|
||||||
|
detached,
|
||||||
|
lock,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
@@ -1136,7 +1266,18 @@ pub fn add_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn remove_worktree(
|
pub async fn remove_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
force: Option<bool>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not remove worktree", move || {
|
||||||
|
remove_worktree_core(path, worktree_path, force)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
force: Option<bool>,
|
force: Option<bool>,
|
||||||
@@ -1168,7 +1309,18 @@ pub fn remove_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn move_worktree(
|
pub async fn move_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
destination: String,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not move worktree", move || {
|
||||||
|
move_worktree_core(path, worktree_path, destination)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
destination: String,
|
destination: String,
|
||||||
@@ -1191,7 +1343,18 @@ pub fn move_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn lock_worktree(
|
pub async fn lock_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
reason: Option<String>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not lock worktree", move || {
|
||||||
|
lock_worktree_core(path, worktree_path, reason)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
@@ -1211,28 +1374,59 @@ pub fn lock_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn unlock_worktree(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn unlock_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not unlock worktree", move || {
|
||||||
|
unlock_worktree_core(path, worktree_path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_worktree_core(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
run_git(&repo, ["worktree", "unlock", "--", worktree_path.as_str()])?;
|
run_git(&repo, ["worktree", "unlock", "--", worktree_path.as_str()])?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn prune_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn prune_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not prune worktrees", move || {
|
||||||
run_git(&repo, ["worktree", "prune"])?;
|
let repo = resolve_repo(&path)?;
|
||||||
worktrees_for_repo(&repo)
|
run_git(&repo, ["worktree", "prune"])?;
|
||||||
|
worktrees_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn repair_worktree(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn repair_worktree(
|
||||||
let repo = resolve_repo(&path)?;
|
path: String,
|
||||||
run_git(&repo, ["worktree", "repair", "--", worktree_path.as_str()])?;
|
worktree_path: String,
|
||||||
worktrees_for_repo(&repo)
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not repair worktree", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
run_git(&repo, ["worktree", "repair", "--", worktree_path.as_str()])?;
|
||||||
|
worktrees_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_tag(
|
pub async fn create_tag(
|
||||||
|
path: String,
|
||||||
|
name: String,
|
||||||
|
target: Option<String>,
|
||||||
|
message: Option<String>,
|
||||||
|
) -> Result<Vec<GitTag>, String> {
|
||||||
|
run_git_task("Could not create tag", move || {
|
||||||
|
create_tag_core(path, name, target, message)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tag_core(
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
@@ -1271,36 +1465,42 @@ pub fn create_tag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_tag(path: String, name: String) -> Result<Vec<GitTag>, String> {
|
pub async fn delete_tag(path: String, name: String) -> Result<Vec<GitTag>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not delete tag", move || {
|
||||||
let name = validate_existing_tag_name(&repo, &name)?;
|
let repo = resolve_repo(&path)?;
|
||||||
run_git(&repo, ["tag", "-d", name.as_str()])?;
|
let name = validate_existing_tag_name(&repo, &name)?;
|
||||||
tags_for_repo(&repo)
|
run_git(&repo, ["tag", "-d", name.as_str()])?;
|
||||||
|
tags_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn push_tag(
|
pub async fn push_tag(
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not push tag", move || {
|
||||||
let name = validate_existing_tag_name(&repo, &name)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let remote = initial_push_remote_name(&repo)?;
|
let name = validate_existing_tag_name(&repo, &name)?;
|
||||||
let tag_ref = format!("refs/tags/{name}");
|
let remote = initial_push_remote_name(&repo)?;
|
||||||
let push_args = ["push", remote.as_str(), tag_ref.as_str()];
|
let tag_ref = format!("refs/tags/{name}");
|
||||||
|
let push_args = ["push", remote.as_str(), tag_ref.as_str()];
|
||||||
|
|
||||||
match (username.as_deref(), password.as_deref()) {
|
match (username.as_deref(), password.as_deref()) {
|
||||||
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
|
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
|
||||||
run_git_authenticated(&repo, push_args, u, p)?;
|
run_git_authenticated(&repo, push_args, u, p)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
run_git(&repo, push_args)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
run_git(&repo, push_args)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_new_tag_name(repo: &Path, name: &str) -> Result<String, String> {
|
fn validate_new_tag_name(repo: &Path, name: &str) -> Result<String, String> {
|
||||||
@@ -1435,7 +1635,7 @@ pub async fn restore_files(
|
|||||||
.map_err(|err| format!("Could not restore files: {err}"))?
|
.map_err(|err| format!("Could not restore files: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_file_patch(path: String, file: String, staged: bool) -> Result<String, String> {
|
pub fn get_file_patch(path: String, file: String, staged: bool) -> Result<String, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -1866,7 +2066,7 @@ pub async fn commit_ai_review(
|
|||||||
parse_ai_review(&raw)
|
parse_ai_review(&raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn apply_file_patch(
|
pub fn apply_file_patch(
|
||||||
path: String,
|
path: String,
|
||||||
file: String,
|
file: String,
|
||||||
@@ -1951,7 +2151,7 @@ pub async fn amend_commit(path: String, message: Option<String>) -> Result<GitSt
|
|||||||
.map_err(|err| format!("Could not amend commit: {err}"))?
|
.map_err(|err| format!("Could not amend commit: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if verify_commit(&repo, "HEAD").is_err() {
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
@@ -1967,7 +2167,7 @@ pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn undo_last_commit(path: String) -> Result<GitStatus, String> {
|
pub fn undo_last_commit(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if verify_commit(&repo, "HEAD").is_err() {
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
@@ -2146,7 +2346,7 @@ fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
|||||||
|
|
||||||
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
||||||
/// current branch, falling back to `origin`, then the first configured remote).
|
/// current branch, falling back to `origin`, then the first configured remote).
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_remote_url(path: String) -> Result<Option<String>, String> {
|
pub fn get_remote_url(path: String) -> Result<Option<String>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let remote = upstream_remote_name(&repo).unwrap_or_else(|| "origin".to_string());
|
let remote = upstream_remote_name(&repo).unwrap_or_else(|| "origin".to_string());
|
||||||
@@ -2301,7 +2501,7 @@ fn push_args_for_repo_to(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
match entry.get_password() {
|
match entry.get_password() {
|
||||||
@@ -2315,7 +2515,7 @@ pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_save(
|
pub fn cred_save(
|
||||||
key: String,
|
key: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -2336,7 +2536,7 @@ pub fn cred_save(
|
|||||||
.map_err(|err| format!("Saving to keychain failed: {err}"))
|
.map_err(|err| format!("Saving to keychain failed: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_delete(key: String) -> Result<(), String> {
|
pub fn cred_delete(key: String) -> Result<(), String> {
|
||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
match entry.delete_credential() {
|
match entry.delete_credential() {
|
||||||
@@ -2402,7 +2602,7 @@ pub async fn merge_branch(
|
|||||||
.map_err(|err| format!("Could not merge: {err}"))?
|
.map_err(|err| format!("Could not merge: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !merge_in_progress(&repo) {
|
if !merge_in_progress(&repo) {
|
||||||
@@ -2415,7 +2615,7 @@ pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !merge_in_progress(&repo) {
|
if !merge_in_progress(&repo) {
|
||||||
@@ -2425,7 +2625,7 @@ pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn revert_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
pub fn revert_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let commit = verify_commit(&repo, &commit)?;
|
let commit = verify_commit(&repo, &commit)?;
|
||||||
@@ -2470,7 +2670,7 @@ pub async fn rebase_branch(path: String, branch: String) -> Result<GitStatus, St
|
|||||||
.map_err(|err| format!("Could not rebase: {err}"))?
|
.map_err(|err| format!("Could not rebase: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_interactive_rebase_commits(
|
pub fn list_interactive_rebase_commits(
|
||||||
path: String,
|
path: String,
|
||||||
base: String,
|
base: String,
|
||||||
@@ -2550,7 +2750,7 @@ pub async fn start_interactive_rebase(
|
|||||||
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
|
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !rebase_in_progress(&repo) {
|
if !rebase_in_progress(&repo) {
|
||||||
@@ -2580,7 +2780,7 @@ pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !rebase_in_progress(&repo) {
|
if !rebase_in_progress(&repo) {
|
||||||
@@ -2592,7 +2792,7 @@ pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, String> {
|
pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let limit = limit.unwrap_or(250).clamp(1, 1_000).to_string();
|
let limit = limit.unwrap_or(250).clamp(1, 1_000).to_string();
|
||||||
@@ -2610,7 +2810,7 @@ pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>,
|
|||||||
parse_reflog(&output)
|
parse_reflog(&output)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_reflog_entry(
|
pub fn restore_reflog_entry(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -2870,7 +3070,7 @@ pub async fn cherry_pick_commit(path: String, commit: String) -> Result<GitStatu
|
|||||||
.map_err(|err| format!("Could not cherry-pick: {err}"))?
|
.map_err(|err| format!("Could not cherry-pick: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !cherry_pick_in_progress(&repo) {
|
if !cherry_pick_in_progress(&repo) {
|
||||||
@@ -2888,7 +3088,7 @@ pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
cherry_pick_status_or_error(&repo, output, "Cherry-pick continue failed")
|
cherry_pick_status_or_error(&repo, output, "Cherry-pick continue failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cherry_pick_abort(path: String) -> Result<GitStatus, String> {
|
pub fn cherry_pick_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !cherry_pick_in_progress(&repo) {
|
if !cherry_pick_in_progress(&repo) {
|
||||||
@@ -2917,13 +3117,16 @@ fn cherry_pick_status_or_error(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_commits(
|
pub async fn list_commits(
|
||||||
path: String,
|
path: String,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
skip: Option<u32>,
|
skip: Option<u32>,
|
||||||
) -> Result<Vec<GitCommit>, String> {
|
) -> Result<Vec<GitCommit>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not load commit history", move || {
|
||||||
commit_page_for_repo(&repo, limit, skip)
|
let repo = resolve_repo(&path)?;
|
||||||
|
commit_page_for_repo(&repo, limit, skip)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
|
fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
|
||||||
@@ -2968,9 +3171,12 @@ fn commit_page_for_repo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile>, String> {
|
pub async fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
run_git_task("Could not load repository files", move || {
|
||||||
repository_files(&repo)
|
let repo = resolve_repo(&path)?;
|
||||||
|
repository_files(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -3020,7 +3226,7 @@ pub fn cancel_file_history(
|
|||||||
|
|
||||||
const UNCOMMITTED_BLAME_HASH: &str = "0000000000000000000000000000000000000000";
|
const UNCOMMITTED_BLAME_HASH: &str = "0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_file_blame(path: String, file: String) -> Result<GitBlameResult, String> {
|
pub fn get_file_blame(path: String, file: String) -> Result<GitBlameResult, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -3318,7 +3524,7 @@ fn search_code_introductions_core(
|
|||||||
Ok(hits)
|
Ok(hits)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let commit_hash = verify_commit(&repo, &commit)?;
|
let commit_hash = verify_commit(&repo, &commit)?;
|
||||||
@@ -3341,7 +3547,7 @@ pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, Stri
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_file_from_commit(
|
pub fn restore_file_from_commit(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3359,7 +3565,7 @@ pub fn restore_file_from_commit(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_commits(
|
pub fn compare_commits(
|
||||||
path: String,
|
path: String,
|
||||||
from: String,
|
from: String,
|
||||||
@@ -3417,7 +3623,7 @@ pub fn compare_commits(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn diff_file_against_working_tree(
|
pub fn diff_file_against_working_tree(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3463,7 +3669,7 @@ pub fn diff_file_against_working_tree(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_file_to_head(
|
pub fn compare_file_to_head(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3525,7 +3731,7 @@ pub fn compare_file_to_head(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_file_to_parent(
|
pub fn compare_file_to_parent(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3599,7 +3805,7 @@ pub fn compare_file_to_parent(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String> {
|
pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -3635,7 +3841,7 @@ pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn resolve_conflict_side(
|
pub fn resolve_conflict_side(
|
||||||
path: String,
|
path: String,
|
||||||
file: String,
|
file: String,
|
||||||
@@ -3677,7 +3883,7 @@ fn index_stage_size(repo: &Path, stage: u8, file: &str) -> Option<u64> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn resolve_conflict(path: String, file: String, content: String) -> Result<GitStatus, String> {
|
pub fn resolve_conflict(path: String, file: String, content: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -4073,21 +4279,7 @@ fn clone_repository_core(
|
|||||||
run_git_clone(remote_url.trim(), &target, username, password)?;
|
run_git_clone(remote_url.trim(), &target, username, password)?;
|
||||||
|
|
||||||
let repo = resolve_repo(&target.to_string_lossy())?;
|
let repo = resolve_repo(&target.to_string_lossy())?;
|
||||||
let status = status_for_repo(&repo)?;
|
repository_bundle_for_repo(&repo, commit_limit)
|
||||||
let branches = branches_for_repo(&repo)?;
|
|
||||||
let tags = tags_for_repo(&repo)?;
|
|
||||||
let stashes = stashes_for_repo(&repo)?;
|
|
||||||
let commits = commits_for_repo(&repo, commit_limit)?;
|
|
||||||
let files = repository_files_with_status(&repo, &status)?;
|
|
||||||
|
|
||||||
Ok(RepositoryBundle {
|
|
||||||
status,
|
|
||||||
branches,
|
|
||||||
tags,
|
|
||||||
stashes,
|
|
||||||
commits,
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_target_path(
|
fn clone_target_path(
|
||||||
@@ -6747,7 +6939,7 @@ mod tests {
|
|||||||
let repo = init_temp_repo("create_branch");
|
let repo = init_temp_repo("create_branch");
|
||||||
commit_initial_file(&repo.path);
|
commit_initial_file(&repo.path);
|
||||||
|
|
||||||
let status = create_branch(
|
let status = create_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6760,7 +6952,7 @@ mod tests {
|
|||||||
"new branch should exist"
|
"new branch should exist"
|
||||||
);
|
);
|
||||||
|
|
||||||
let err = create_branch(
|
let err = create_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6776,7 +6968,7 @@ mod tests {
|
|||||||
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
run_git_test(&repo.path, ["branch", "feature/old-panel"]);
|
run_git_test(&repo.path, ["branch", "feature/old-panel"]);
|
||||||
|
|
||||||
let status = rename_branch(
|
let status = rename_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/old-panel".to_string(),
|
"feature/old-panel".to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
@@ -6801,7 +6993,7 @@ mod tests {
|
|||||||
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
run_git_test(&repo.path, ["branch", "stale"]);
|
run_git_test(&repo.path, ["branch", "stale"]);
|
||||||
|
|
||||||
let status = delete_branch(
|
let status = delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"stale".to_string(),
|
"stale".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6815,7 +7007,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err =
|
||||||
delete_branch(repo.path.to_string_lossy().to_string(), current, None).unwrap_err();
|
delete_branch_core(repo.path.to_string_lossy().to_string(), current, None).unwrap_err();
|
||||||
assert!(err.contains("current branch"));
|
assert!(err.contains("current branch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6831,7 +7023,7 @@ mod tests {
|
|||||||
run_git_test(&repo.path, ["commit", "-q", "-m", "feature"]);
|
run_git_test(&repo.path, ["commit", "-q", "-m", "feature"]);
|
||||||
run_git_test(&repo.path, ["checkout", "-q", current.as_str()]);
|
run_git_test(&repo.path, ["checkout", "-q", current.as_str()]);
|
||||||
|
|
||||||
let err = delete_branch(
|
let err = delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/unmerged".to_string(),
|
"feature/unmerged".to_string(),
|
||||||
Some(false),
|
Some(false),
|
||||||
@@ -6839,7 +7031,7 @@ mod tests {
|
|||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.contains("not fully merged"));
|
assert!(err.contains("not fully merged"));
|
||||||
|
|
||||||
delete_branch(
|
delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/unmerged".to_string(),
|
"feature/unmerged".to_string(),
|
||||||
Some(true),
|
Some(true),
|
||||||
@@ -7216,7 +7408,7 @@ mod tests {
|
|||||||
commit_initial_file(&repo.path);
|
commit_initial_file(&repo.path);
|
||||||
run_git_test(&repo.path, ["branch", "feature"]);
|
run_git_test(&repo.path, ["branch", "feature"]);
|
||||||
|
|
||||||
let rows = add_worktree(
|
let rows = add_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some("feature".to_string()),
|
Some("feature".to_string()),
|
||||||
@@ -7234,7 +7426,7 @@ mod tests {
|
|||||||
assert!(!linked.is_main);
|
assert!(!linked.is_main);
|
||||||
assert!(linked.clean);
|
assert!(linked.clean);
|
||||||
|
|
||||||
let rows = lock_worktree(
|
let rows = lock_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some("test lock".to_string()),
|
Some("test lock".to_string()),
|
||||||
@@ -7247,13 +7439,13 @@ mod tests {
|
|||||||
assert!(linked.locked);
|
assert!(linked.locked);
|
||||||
assert_eq!(linked.lock_reason.as_deref(), Some("test lock"));
|
assert_eq!(linked.lock_reason.as_deref(), Some("test lock"));
|
||||||
|
|
||||||
unlock_worktree(
|
unlock_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
)
|
)
|
||||||
.expect("worktree should unlock");
|
.expect("worktree should unlock");
|
||||||
|
|
||||||
let rows = remove_worktree(
|
let rows = remove_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some(false),
|
Some(false),
|
||||||
|
|||||||
+147
-160
@@ -153,6 +153,7 @@
|
|||||||
RebaseCommit,
|
RebaseCommit,
|
||||||
RebasePlanItem,
|
RebasePlanItem,
|
||||||
ReflogEntry,
|
ReflogEntry,
|
||||||
|
RepositoryBundle,
|
||||||
StoredCredential,
|
StoredCredential,
|
||||||
} from "./lib/types";
|
} from "./lib/types";
|
||||||
|
|
||||||
@@ -204,6 +205,15 @@
|
|||||||
clearIfCurrent: (message: string) => void;
|
clearIfCurrent: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RepositoryRefreshOptions {
|
||||||
|
branches?: boolean;
|
||||||
|
tags?: boolean;
|
||||||
|
stashes?: boolean;
|
||||||
|
commits?: boolean;
|
||||||
|
files?: boolean;
|
||||||
|
fileHistory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
||||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||||
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
|
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
|
||||||
@@ -372,7 +382,6 @@
|
|||||||
const BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS = 30_000;
|
const BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS = 30_000;
|
||||||
const BACKGROUND_FETCH_OTHER_BATCH_SIZE = 2;
|
const BACKGROUND_FETCH_OTHER_BATCH_SIZE = 2;
|
||||||
const STARTUP_SPLASH_MIN_VISIBLE_MS = 850;
|
const STARTUP_SPLASH_MIN_VISIBLE_MS = 850;
|
||||||
const STARTUP_FETCH_MAX_WAIT_MS = 20_000;
|
|
||||||
let backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
let backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundFetchInFlight = false;
|
let backgroundFetchInFlight = false;
|
||||||
@@ -482,12 +491,16 @@
|
|||||||
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
||||||
window.addEventListener("beforeunload", handleAppShutdown);
|
window.addEventListener("beforeunload", handleAppShutdown);
|
||||||
window.addEventListener("pagehide", handleAppShutdown);
|
window.addEventListener("pagehide", handleAppShutdown);
|
||||||
void getCurrentWindow().onCloseRequested(() => handleAppShutdown()).then((unlisten) => {
|
try {
|
||||||
if (appShuttingDown) unlisten();
|
void getCurrentWindow().onCloseRequested(() => handleAppShutdown()).then((unlisten) => {
|
||||||
else unlistenCloseRequested = unlisten;
|
if (appShuttingDown) unlisten();
|
||||||
}).catch(() => {
|
else unlistenCloseRequested = unlisten;
|
||||||
// Browser preview has no Tauri window; DOM lifecycle events still cover it.
|
}).catch(() => {
|
||||||
});
|
// Browser preview has no Tauri window; DOM lifecycle events still cover it.
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// getCurrentWindow itself throws synchronously in a plain browser preview.
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -568,16 +581,15 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await waitForStartupPaint();
|
await waitForStartupPaint();
|
||||||
await Promise.race([
|
|
||||||
fetchOpenRepositoriesDuringStartup(),
|
|
||||||
wait(STARTUP_FETCH_MAX_WAIT_MS),
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
const remainingSplashTime = STARTUP_SPLASH_MIN_VISIBLE_MS - (performance.now() - startupStartedAt);
|
const remainingSplashTime = STARTUP_SPLASH_MIN_VISIBLE_MS - (performance.now() - startupStartedAt);
|
||||||
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
||||||
|
|
||||||
await closeStartupSplashscreen();
|
await closeStartupSplashscreen();
|
||||||
startBackgroundTimers();
|
startBackgroundTimers();
|
||||||
|
// Remote access can take seconds (offline networks, SSH negotiation,
|
||||||
|
// credential helpers). It must never hold the startup screen hostage.
|
||||||
|
void fetchOpenRepositoriesDuringStartup();
|
||||||
void backgroundRepoStatusTick(false);
|
void backgroundRepoStatusTick(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,23 +777,14 @@
|
|||||||
// now-active repo's name and data with this one's.
|
// now-active repo's name and data with this one's.
|
||||||
if (!sameRepoPath(path, activeRepoPath)) return;
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
||||||
applyStatus(nextStatus);
|
|
||||||
// Something changed — reload branches, commits and files in one bundled call.
|
// Something changed — reload branches, commits and files in one bundled call.
|
||||||
const bundle = await openRepositoryBundle(path, Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1);
|
const bundle = await openRepositoryBundle(path, Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1);
|
||||||
if (!sameRepoPath(path, activeRepoPath)) return;
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
const previousHeadHash = lastFileHistoryHeadHash;
|
await applyRepositoryBundle(path, bundle);
|
||||||
await refreshBranchList(path, bundle.branches);
|
|
||||||
await refreshTags(path, bundle.tags);
|
|
||||||
await refreshStashes(path, bundle.stashes);
|
|
||||||
await refreshCommitHistory(path, bundle.commits);
|
|
||||||
await refreshExplorerFiles(path, bundle.files);
|
|
||||||
// File history reflects `git log`, which only changes when HEAD actually moves
|
// File history reflects `git log`, which only changes when HEAD actually moves
|
||||||
// (new commit, checkout, merge, ...) — skip the reload otherwise so a plain
|
// (new commit, checkout, merge, ...) — skip the reload otherwise so a plain
|
||||||
// working-tree/status change (staging, edits) doesn't keep re-fetching and
|
// working-tree/status change (staging, edits) doesn't keep re-fetching and
|
||||||
// flickering the currently viewed file's history.
|
// flickering the currently viewed file's history.
|
||||||
if (lastFileHistoryHeadHash !== previousHeadHash && sameRepoPath(path, activeRepoPath)) {
|
|
||||||
await refreshFileHistory(path);
|
|
||||||
}
|
|
||||||
} catch { /* ignore transient errors */ } finally {
|
} catch { /* ignore transient errors */ } finally {
|
||||||
autoRefreshInFlight = false;
|
autoRefreshInFlight = false;
|
||||||
}
|
}
|
||||||
@@ -1114,10 +1117,7 @@
|
|||||||
aiCommitSplitOpen = false;
|
aiCommitSplitOpen = false;
|
||||||
aiCommitPlan = null;
|
aiCommitPlan = null;
|
||||||
commitMessage = "";
|
commitMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("ai_commit_split_applied", { groups: plan.groups.length, files: allFiles.length });
|
trackEvent("ai_commit_split_applied", { groups: plan.groups.length, files: allFiles.length });
|
||||||
});
|
});
|
||||||
commitAiSplitting = false;
|
commitAiSplitting = false;
|
||||||
@@ -1895,15 +1895,21 @@
|
|||||||
// ── Refresh helpers ────────────────────────────────────────────────────────
|
// ── Refresh helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function refreshBranchList(path = activeRepoPath, prefetched?: GitBranchInfo[]) {
|
async function refreshBranchList(path = activeRepoPath, prefetched?: GitBranchInfo[]) {
|
||||||
branches = prefetched ?? (await listBranches(path));
|
const nextBranches = prefetched ?? (await listBranches(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
branches = nextBranches;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTags(path = activeRepoPath, prefetched?: GitTag[]) {
|
async function refreshTags(path = activeRepoPath, prefetched?: GitTag[]) {
|
||||||
tags = prefetched ?? (await listTags(path));
|
const nextTags = prefetched ?? (await listTags(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
tags = nextTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStashes(path = activeRepoPath, prefetched?: GitStash[]) {
|
async function refreshStashes(path = activeRepoPath, prefetched?: GitStash[]) {
|
||||||
stashes = prefetched ?? (await listStashes(path));
|
const nextStashes = prefetched ?? (await listStashes(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
stashes = nextStashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
||||||
@@ -1960,7 +1966,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
||||||
repoFiles = prefetched ?? (await listRepositoryFiles(path));
|
const nextFiles = prefetched ?? (await listRepositoryFiles(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
repoFiles = nextFiles;
|
||||||
const folderPaths = allExplorerFolderPaths(repoFiles);
|
const folderPaths = allExplorerFolderPaths(repoFiles);
|
||||||
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
||||||
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
||||||
@@ -1970,11 +1978,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshRepositoryViews(
|
||||||
|
path = activeRepoPath,
|
||||||
|
options: RepositoryRefreshOptions = {},
|
||||||
|
) {
|
||||||
|
const tasks: Promise<void>[] = [];
|
||||||
|
if (options.branches ?? true) tasks.push(refreshBranchList(path));
|
||||||
|
if (options.tags ?? false) tasks.push(refreshTags(path));
|
||||||
|
if (options.stashes ?? false) tasks.push(refreshStashes(path));
|
||||||
|
if (options.commits ?? true) tasks.push(refreshCommitHistory(path));
|
||||||
|
if (options.files ?? true) tasks.push(refreshExplorerFiles(path));
|
||||||
|
|
||||||
|
// These reads do not depend on each other. Starting them together removes
|
||||||
|
// several IPC/Git-process waterfalls after every user operation.
|
||||||
|
await Promise.all(tasks);
|
||||||
|
|
||||||
|
// Explorer refresh may invalidate the selected path, so file history runs
|
||||||
|
// after the parallel group rather than racing a disappearing selection.
|
||||||
|
if (options.fileHistory ?? true) await refreshFileHistory(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyRepositoryBundle(
|
||||||
|
path: string,
|
||||||
|
bundle: RepositoryBundle,
|
||||||
|
forceFileHistory = false,
|
||||||
|
) {
|
||||||
|
const previousHeadHash = lastFileHistoryHeadHash;
|
||||||
|
applyStatus(bundle.status);
|
||||||
|
const resolvedPath = activeRepoPath || path;
|
||||||
|
await Promise.all([
|
||||||
|
refreshBranchList(resolvedPath, bundle.branches),
|
||||||
|
refreshTags(resolvedPath, bundle.tags),
|
||||||
|
refreshStashes(resolvedPath, bundle.stashes),
|
||||||
|
refreshCommitHistory(resolvedPath, bundle.commits),
|
||||||
|
refreshExplorerFiles(resolvedPath, bundle.files),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (forceFileHistory || lastFileHistoryHeadHash !== previousHeadHash) {
|
||||||
|
await refreshFileHistory(resolvedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshRepositorySnapshot(path = activeRepoPath, forceFileHistory = false) {
|
||||||
|
const bundle = await openRepositoryBundle(
|
||||||
|
path,
|
||||||
|
Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1,
|
||||||
|
);
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
await applyRepositoryBundle(path, bundle, forceFileHistory);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshRefsAndCommitGraph(path = activeRepoPath) {
|
async function refreshRefsAndCommitGraph(path = activeRepoPath) {
|
||||||
const previousHeadHash = lastFileHistoryHeadHash;
|
const previousHeadHash = lastFileHistoryHeadHash;
|
||||||
await refreshBranchList(path);
|
await Promise.all([
|
||||||
await refreshTags(path);
|
refreshBranchList(path),
|
||||||
await refreshCommitHistory(path);
|
refreshTags(path),
|
||||||
|
refreshCommitHistory(path),
|
||||||
|
]);
|
||||||
if (lastFileHistoryHeadHash !== previousHeadHash && fileHistoryDialogOpen) {
|
if (lastFileHistoryHeadHash !== previousHeadHash && fileHistoryDialogOpen) {
|
||||||
await refreshFileHistory(path);
|
await refreshFileHistory(path);
|
||||||
}
|
}
|
||||||
@@ -2050,14 +2110,9 @@
|
|||||||
const bundle = await openRepositoryBundle(path, COMMIT_HISTORY_PAGE_SIZE + 1);
|
const bundle = await openRepositoryBundle(path, COMMIT_HISTORY_PAGE_SIZE + 1);
|
||||||
if (requestId !== repoOpenRequestId) return;
|
if (requestId !== repoOpenRequestId) return;
|
||||||
resetRepositoryState(false);
|
resetRepositoryState(false);
|
||||||
applyStatus(bundle.status);
|
await applyRepositoryBundle(path, bundle);
|
||||||
if (globalSearchBusy) void cancelGlobalSearch();
|
if (globalSearchBusy) void cancelGlobalSearch();
|
||||||
activeView = "repository";
|
activeView = "repository";
|
||||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
|
||||||
await refreshTags(activeRepoPath, bundle.tags);
|
|
||||||
await refreshStashes(activeRepoPath, bundle.stashes);
|
|
||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
|
||||||
lastRepoSwitchAt = Date.now();
|
lastRepoSwitchAt = Date.now();
|
||||||
trackEvent("repository_opened", {
|
trackEvent("repository_opened", {
|
||||||
changed_files: bundle.status.files.length,
|
changed_files: bundle.status.files.length,
|
||||||
@@ -2124,14 +2179,9 @@
|
|||||||
COMMIT_HISTORY_PAGE_SIZE + 1,
|
COMMIT_HISTORY_PAGE_SIZE + 1,
|
||||||
);
|
);
|
||||||
resetRepositoryState(false);
|
resetRepositoryState(false);
|
||||||
applyStatus(bundle.status);
|
await applyRepositoryBundle(bundle.status.repo_path, bundle);
|
||||||
if (globalSearchBusy) void cancelGlobalSearch();
|
if (globalSearchBusy) void cancelGlobalSearch();
|
||||||
activeView = "repository";
|
activeView = "repository";
|
||||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
|
||||||
await refreshTags(activeRepoPath, bundle.tags);
|
|
||||||
await refreshStashes(activeRepoPath, bundle.stashes);
|
|
||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
|
||||||
cloneDialogOpen = false;
|
cloneDialogOpen = false;
|
||||||
pendingClone = null;
|
pendingClone = null;
|
||||||
if (credDialogAction === "clone") {
|
if (credDialogAction === "clone") {
|
||||||
@@ -2350,12 +2400,7 @@
|
|||||||
async function refreshRepo() {
|
async function refreshRepo() {
|
||||||
if (!activeRepoPath) { await openRepo(); return; }
|
if (!activeRepoPath) { await openRepo(); return; }
|
||||||
await runOperation("Refreshing", async () => {
|
await runOperation("Refreshing", async () => {
|
||||||
applyStatus(await getStatus(activeRepoPath));
|
await refreshRepositorySnapshot(activeRepoPath, true);
|
||||||
await refreshBranchList(activeRepoPath);
|
|
||||||
await refreshStashes(activeRepoPath);
|
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_refreshed", {
|
trackEvent("repository_refreshed", {
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
});
|
});
|
||||||
@@ -2366,10 +2411,7 @@
|
|||||||
if (!activeRepoPath || branch.current) return;
|
if (!activeRepoPath || branch.current) return;
|
||||||
await runOperation(`Checking out ${branch.name}`, async () => {
|
await runOperation(`Checking out ${branch.name}`, async () => {
|
||||||
applyStatus(await checkoutBranch(activeRepoPath, branch.name));
|
applyStatus(await checkoutBranch(activeRepoPath, branch.name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_checked_out", {
|
trackEvent("branch_checked_out", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2381,10 +2423,7 @@
|
|||||||
if (!activeRepoPath || !name) return;
|
if (!activeRepoPath || !name) return;
|
||||||
await runOperation(`Creating ${name}`, async () => {
|
await runOperation(`Creating ${name}`, async () => {
|
||||||
applyStatus(await createBranch(activeRepoPath, name));
|
applyStatus(await createBranch(activeRepoPath, name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_created");
|
trackEvent("branch_created");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2403,10 +2442,7 @@
|
|||||||
await runOperation(`Renaming ${branch.name}`, async () => {
|
await runOperation(`Renaming ${branch.name}`, async () => {
|
||||||
applyStatus(await renameBranch(activeRepoPath, branch.name, name));
|
applyStatus(await renameBranch(activeRepoPath, branch.name, name));
|
||||||
renameBranchTarget = null;
|
renameBranchTarget = null;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_renamed");
|
trackEvent("branch_renamed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2454,10 +2490,7 @@
|
|||||||
applyStatus(await deleteBranch(activeRepoPath, branch.name, deleteBranchForce));
|
applyStatus(await deleteBranch(activeRepoPath, branch.name, deleteBranchForce));
|
||||||
deleteBranchTarget = null;
|
deleteBranchTarget = null;
|
||||||
deleteBranchForce = false;
|
deleteBranchForce = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_deleted", {
|
trackEvent("branch_deleted", {
|
||||||
force: forceDelete ? 1 : 0,
|
force: forceDelete ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2633,10 +2666,7 @@
|
|||||||
await runOperation(`Creating ${name}`, async () => {
|
await runOperation(`Creating ${name}`, async () => {
|
||||||
applyStatus(await createBranch(activeRepoPath, name, target.hash));
|
applyStatus(await createBranch(activeRepoPath, name, target.hash));
|
||||||
newBranchCommit = null;
|
newBranchCommit = null;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_created_from_commit");
|
trackEvent("branch_created_from_commit");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2648,10 +2678,7 @@
|
|||||||
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
||||||
await runOperation(`Merging ${branch.name}`, async () => {
|
await runOperation(`Merging ${branch.name}`, async () => {
|
||||||
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_merged", {
|
trackEvent("branch_merged", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2662,10 +2689,7 @@
|
|||||||
if (!activeRepoPath || branch.current || rebaseInProgress || cherryPickInProgress) return;
|
if (!activeRepoPath || branch.current || rebaseInProgress || cherryPickInProgress) return;
|
||||||
await runOperation(`Rebasing onto ${branch.name}`, async () => {
|
await runOperation(`Rebasing onto ${branch.name}`, async () => {
|
||||||
applyStatus(await rebaseBranch(activeRepoPath, branch.name));
|
applyStatus(await rebaseBranch(activeRepoPath, branch.name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_rebased", {
|
trackEvent("branch_rebased", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2676,10 +2700,7 @@
|
|||||||
if (!activeRepoPath || !rebaseInProgress || hasConflicts) return;
|
if (!activeRepoPath || !rebaseInProgress || hasConflicts) return;
|
||||||
await runOperation("Continuing rebase", async () => {
|
await runOperation("Continuing rebase", async () => {
|
||||||
applyStatus(await rebaseContinue(activeRepoPath));
|
applyStatus(await rebaseContinue(activeRepoPath));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("rebase_continued");
|
trackEvent("rebase_continued");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2695,10 +2716,7 @@
|
|||||||
resolveDialogOpen = false;
|
resolveDialogOpen = false;
|
||||||
conflict = null;
|
conflict = null;
|
||||||
conflictTarget = "";
|
conflictTarget = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("rebase_aborted");
|
trackEvent("rebase_aborted");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2742,10 +2760,7 @@
|
|||||||
await runOperation("Starting interactive rebase", async () => {
|
await runOperation("Starting interactive rebase", async () => {
|
||||||
applyStatus(await startInteractiveRebase(activeRepoPath, interactiveRebaseBase, plan));
|
applyStatus(await startInteractiveRebase(activeRepoPath, interactiveRebaseBase, plan));
|
||||||
interactiveRebaseOpen = false;
|
interactiveRebaseOpen = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("interactive_rebase_started", { commits: plan.length });
|
trackEvent("interactive_rebase_started", { commits: plan.length });
|
||||||
});
|
});
|
||||||
if (interactiveRebaseOpen && errorMessage) interactiveRebaseError = errorMessage;
|
if (interactiveRebaseOpen && errorMessage) interactiveRebaseError = errorMessage;
|
||||||
@@ -2787,10 +2802,7 @@
|
|||||||
await runOperation("Restoring reflog entry", async () => {
|
await runOperation("Restoring reflog entry", async () => {
|
||||||
applyStatus(await restoreReflogEntry(activeRepoPath, entry.hash, branch.trim()));
|
applyStatus(await restoreReflogEntry(activeRepoPath, entry.hash, branch.trim()));
|
||||||
reflogOpen = false;
|
reflogOpen = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("reflog_recovered");
|
trackEvent("reflog_recovered");
|
||||||
});
|
});
|
||||||
if (reflogOpen && errorMessage) reflogError = errorMessage;
|
if (reflogOpen && errorMessage) reflogError = errorMessage;
|
||||||
@@ -2843,10 +2855,7 @@
|
|||||||
if (!activeRepoPath || rebaseInProgress || cherryPickInProgress) return;
|
if (!activeRepoPath || rebaseInProgress || cherryPickInProgress) return;
|
||||||
await runOperation(`Cherry-picking ${commit.short_hash}`, async () => {
|
await runOperation(`Cherry-picking ${commit.short_hash}`, async () => {
|
||||||
applyStatus(await cherryPickCommit(activeRepoPath, commit.hash));
|
applyStatus(await cherryPickCommit(activeRepoPath, commit.hash));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_cherry_picked");
|
trackEvent("commit_cherry_picked");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2855,10 +2864,7 @@
|
|||||||
if (!activeRepoPath || !cherryPickInProgress || hasConflicts) return;
|
if (!activeRepoPath || !cherryPickInProgress || hasConflicts) return;
|
||||||
await runOperation("Continuing cherry-pick", async () => {
|
await runOperation("Continuing cherry-pick", async () => {
|
||||||
applyStatus(await cherryPickContinue(activeRepoPath));
|
applyStatus(await cherryPickContinue(activeRepoPath));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("cherry_pick_continued");
|
trackEvent("cherry_pick_continued");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2874,10 +2880,7 @@
|
|||||||
resolveDialogOpen = false;
|
resolveDialogOpen = false;
|
||||||
conflict = null;
|
conflict = null;
|
||||||
conflictTarget = "";
|
conflictTarget = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("cherry_pick_aborted");
|
trackEvent("cherry_pick_aborted");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2953,10 +2956,7 @@
|
|||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
await runOperation("Pulling", async () => {
|
await runOperation("Pulling", async () => {
|
||||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pulled", {
|
trackEvent("repository_pulled", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -2995,9 +2995,7 @@
|
|||||||
await runOperation("Pushing", async () => {
|
await runOperation("Pushing", async () => {
|
||||||
applyStatus(await push(activeRepoPath, username, password, remoteActionForceWithLease, selectedRemote || undefined));
|
applyStatus(await push(activeRepoPath, username, password, remoteActionForceWithLease, selectedRemote || undefined));
|
||||||
remoteActionForceWithLease = false;
|
remoteActionForceWithLease = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { files: false });
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pushed", {
|
trackEvent("repository_pushed", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -3021,10 +3019,7 @@
|
|||||||
|
|
||||||
await runOperation("Pulling before push", async () => {
|
await runOperation("Pulling before push", async () => {
|
||||||
applyStatus(await pull(activeRepoPath, username, password));
|
applyStatus(await pull(activeRepoPath, username, password));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
@@ -3041,9 +3036,7 @@
|
|||||||
|
|
||||||
await runOperation("Pushing after pull", async () => {
|
await runOperation("Pushing after pull", async () => {
|
||||||
applyStatus(await push(activeRepoPath, username, password));
|
applyStatus(await push(activeRepoPath, username, password));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { files: false });
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pushed_after_pull", {
|
trackEvent("repository_pushed_after_pull", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -3137,18 +3130,24 @@
|
|||||||
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
||||||
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
||||||
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
||||||
await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function continueMerge() {
|
async function continueMerge() {
|
||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation("Continuing merge", async () => { applyStatus(await mergeContinue(activeRepoPath)); await refreshCommitHistory(activeRepoPath); });
|
await runOperation("Continuing merge", async () => {
|
||||||
|
applyStatus(await mergeContinue(activeRepoPath));
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function abortMerge() {
|
async function abortMerge() {
|
||||||
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
||||||
await runOperation("Aborting merge", async () => { applyStatus(await mergeAbort(activeRepoPath)); await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); });
|
await runOperation("Aborting merge", async () => {
|
||||||
|
applyStatus(await mergeAbort(activeRepoPath));
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPruneRepo() {
|
async function fetchPruneRepo() {
|
||||||
@@ -3205,9 +3204,11 @@
|
|||||||
const stashedFiles = changedFiles.length;
|
const stashedFiles = changedFiles.length;
|
||||||
await runOperation("Stashing changes", async () => {
|
await runOperation("Stashing changes", async () => {
|
||||||
applyStatus(await stashPush(activeRepoPath, message, includeUntracked));
|
applyStatus(await stashPush(activeRepoPath, message, includeUntracked));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_saved", {
|
trackEvent("stash_saved", {
|
||||||
include_untracked: includeUntracked ? 1 : 0,
|
include_untracked: includeUntracked ? 1 : 0,
|
||||||
changed_files: stashedFiles,
|
changed_files: stashedFiles,
|
||||||
@@ -3219,9 +3220,11 @@
|
|||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation(`Applying ${stash.selector}`, async () => {
|
await runOperation(`Applying ${stash.selector}`, async () => {
|
||||||
applyStatus(await stashApply(activeRepoPath, stash.selector));
|
applyStatus(await stashApply(activeRepoPath, stash.selector));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_applied");
|
trackEvent("stash_applied");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3230,9 +3233,11 @@
|
|||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation(`Popping ${stash.selector}`, async () => {
|
await runOperation(`Popping ${stash.selector}`, async () => {
|
||||||
applyStatus(await stashPop(activeRepoPath, stash.selector));
|
applyStatus(await stashPop(activeRepoPath, stash.selector));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_popped");
|
trackEvent("stash_popped");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3303,8 +3308,7 @@
|
|||||||
const paths = files.map((file) => file.path);
|
const paths = files.map((file) => file.path);
|
||||||
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
|
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
|
||||||
applyStatus(await restoreFiles(activeRepoPath, paths, staged));
|
applyStatus(await restoreFiles(activeRepoPath, paths, staged));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("file_discarded", {
|
trackEvent("file_discarded", {
|
||||||
files: files.length,
|
files: files.length,
|
||||||
staged: staged ? 1 : 0,
|
staged: staged ? 1 : 0,
|
||||||
@@ -3323,8 +3327,7 @@
|
|||||||
if (stagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, stagedPaths, true);
|
if (stagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, stagedPaths, true);
|
||||||
if (unstagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, unstagedPaths, false);
|
if (unstagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, unstagedPaths, false);
|
||||||
if (nextStatus) applyStatus(nextStatus);
|
if (nextStatus) applyStatus(nextStatus);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("file_discarded", {
|
trackEvent("file_discarded", {
|
||||||
files: files.length,
|
files: files.length,
|
||||||
staged: 2,
|
staged: 2,
|
||||||
@@ -3425,8 +3428,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
applyStatus(await applyFilePatch(activeRepoPath, file.path, patch, action));
|
applyStatus(await applyFilePatch(activeRepoPath, file.path, patch, action));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
|
|
||||||
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
||||||
if (updatedPatch.trim()) {
|
if (updatedPatch.trim()) {
|
||||||
@@ -3532,10 +3534,7 @@
|
|||||||
amendMode = false;
|
amendMode = false;
|
||||||
preAmendDraftMessage = "";
|
preAmendDraftMessage = "";
|
||||||
lastLocalAiGeneratedMessage = "";
|
lastLocalAiGeneratedMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_created", { amend: 1 });
|
trackEvent("commit_created", { amend: 1 });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -3546,10 +3545,7 @@
|
|||||||
applyStatus(await commit(activeRepoPath, message));
|
applyStatus(await commit(activeRepoPath, message));
|
||||||
commitMessage = "";
|
commitMessage = "";
|
||||||
lastLocalAiGeneratedMessage = "";
|
lastLocalAiGeneratedMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3591,10 +3587,7 @@
|
|||||||
commitMessage = preAmendDraftMessage;
|
commitMessage = preAmendDraftMessage;
|
||||||
preAmendDraftMessage = "";
|
preAmendDraftMessage = "";
|
||||||
}
|
}
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_undone");
|
trackEvent("commit_undone");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3607,10 +3600,7 @@
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
await runOperation(`Restoring ${target.short_hash}`, async () => {
|
await runOperation(`Restoring ${target.short_hash}`, async () => {
|
||||||
applyStatus(await restoreToCommit(activeRepoPath, target.hash));
|
applyStatus(await restoreToCommit(activeRepoPath, target.hash));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_restored");
|
trackEvent("commit_restored");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3621,8 +3611,7 @@
|
|||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
await runOperation(`Restoring ${file.path}`, async () => {
|
await runOperation(`Restoring ${file.path}`, async () => {
|
||||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
|
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_file_restored");
|
trackEvent("commit_file_restored");
|
||||||
});
|
});
|
||||||
return !errorMessage;
|
return !errorMessage;
|
||||||
@@ -3767,8 +3756,7 @@
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
|
await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
|
||||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
|
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("selected_file_restored_from_commit", {
|
trackEvent("selected_file_restored_from_commit", {
|
||||||
kind,
|
kind,
|
||||||
});
|
});
|
||||||
@@ -3960,8 +3948,7 @@
|
|||||||
}
|
}
|
||||||
preparedResolutions = {};
|
preparedResolutions = {};
|
||||||
if (nextStatus) applyStatus(nextStatus);
|
if (nextStatus) applyStatus(nextStatus);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
|
|
||||||
const remaining = (nextStatus?.files ?? status?.files ?? []).filter(
|
const remaining = (nextStatus?.files ?? status?.files ?? []).filter(
|
||||||
(f) => f.staged === "conflicted" || f.unstaged === "conflicted",
|
(f) => f.staged === "conflicted" || f.unstaged === "conflicted",
|
||||||
|
|||||||
Reference in New Issue
Block a user