Optimize repository loading and enhance Git UI

Consolidate multiple Git backend calls into a single bundle to reduce
overhead when opening and auto-refreshing repositories. This significantly
improves performance by fetching status, branches, commits, and files
in one optimized operation, instead of re-running 'git rev-parse' and
'git status' multiple times.

Also, streamline commit history loading by fetching file changes inline
via 'git log --name-status -z', eliminating expensive per-commit
'git diff-tree' processes.

Additionally, introduce the ability to create new branches from a
specific commit in the history and refactor the commit comparison
feature into a dedicated dialog. The status panel now displays concise
file names.
This commit is contained in:
Christoph Brandau
2026-07-01 11:39:22 +02:00
parent 628e2f7c0b
commit fe392d38bf
12 changed files with 528 additions and 56 deletions
+144 -7
View File
@@ -208,6 +208,41 @@ pub fn open_repository(path: String) -> Result<GitStatus, String> {
status_for_repo(&repo)
}
#[derive(Debug, Clone, Serialize)]
pub struct RepositoryBundle {
pub status: GitStatus,
pub branches: Vec<GitBranch>,
pub commits: Vec<GitCommit>,
pub files: Vec<GitRepositoryFile>,
}
/// Opens a repository and gathers everything the UI needs in a single call.
///
/// Runs on a blocking thread (so the UI/overlay stays responsive) and resolves
/// the repo and its status only once, instead of the previous four separate
/// commands that each re-ran `git rev-parse` and `git status`.
#[tauri::command]
pub async fn open_repository_bundle(
path: String,
commit_limit: Option<u32>,
) -> Result<RepositoryBundle, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<RepositoryBundle, String> {
let repo = resolve_repo(&path)?;
let status = status_for_repo(&repo)?;
let branches = branches_for_repo(&repo)?;
let commits = commits_for_repo(&repo, commit_limit)?;
let files = repository_files_with_status(&repo, &status)?;
Ok(RepositoryBundle {
status,
branches,
commits,
files,
})
})
.await
.map_err(|err| format!("Repository konnte nicht geladen werden: {err}"))?
}
#[tauri::command]
pub fn get_status(path: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
@@ -217,8 +252,12 @@ pub fn get_status(path: String) -> Result<GitStatus, String> {
#[tauri::command]
pub fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
let repo = resolve_repo(&path)?;
branches_for_repo(&repo)
}
fn branches_for_repo(repo: &Path) -> Result<Vec<GitBranch>, String> {
let output = run_git(
&repo,
repo,
[
"for-each-ref",
"--format=%(refname)\t%(HEAD)",
@@ -282,10 +321,23 @@ pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String
}
#[tauri::command]
pub fn create_branch(path: String, branch: String) -> Result<GitStatus, String> {
pub fn create_branch(
path: String,
branch: String,
start_point: Option<String>,
) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let branch = validate_new_branch_name(&repo, &branch)?;
run_git(&repo, ["checkout", "-b", branch.as_str()])?;
match start_point {
Some(start) if !start.trim().is_empty() => {
// Resolve the requested commit first so we fail clearly if it is gone.
let start = verify_commit(&repo, &start)?;
run_git(&repo, ["checkout", "-b", branch.as_str(), start.as_str()])?;
}
_ => {
run_git(&repo, ["checkout", "-b", branch.as_str()])?;
}
}
status_for_repo(&repo)
}
@@ -586,23 +638,34 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
#[tauri::command]
pub fn list_commits(path: String, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
let repo = resolve_repo(&path)?;
if verify_commit(&repo, "HEAD").is_err() {
commits_for_repo(&repo, limit)
}
fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
if verify_commit(repo, "HEAD").is_err() {
return Ok(Vec::new());
}
let limit = limit.unwrap_or(100).clamp(1, 500).to_string();
// Fetch the per-commit changed files inline via `--name-status` in a single
// `git log` process, instead of spawning one `git diff-tree` per commit
// (which was ~100 extra processes and the main cost of opening a repo).
let output = run_git(
&repo,
repo,
[
"log",
"--decorate=short",
"--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%P%x1f%s%x1e",
"--name-status",
"-M",
"-z",
"--root",
"--pretty=format:%x1e%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%P%x1f%s%x1f",
"-n",
limit.as_str(),
],
)?;
parse_commit_log(&repo, &output)
parse_commit_log_inline(&output)
}
#[tauri::command]
@@ -1242,6 +1305,13 @@ fn status_for_repo(repo: &Path) -> Result<GitStatus, String> {
fn repository_files(repo: &Path) -> Result<Vec<GitRepositoryFile>, String> {
let status = status_for_repo(repo)?;
repository_files_with_status(repo, &status)
}
fn repository_files_with_status(
repo: &Path,
status: &GitStatus,
) -> Result<Vec<GitRepositoryFile>, String> {
let mut files = BTreeMap::<String, GitRepositoryFile>::new();
let tracked_output = run_git(repo, ["ls-files", "-z", "--cached", "--deleted"])?;
@@ -1580,6 +1650,71 @@ fn commit_search_metadata(repo: &Path, commit: &str) -> Result<GitSearchCommitMe
})
}
/// Parses `git log --name-status -z` output where each commit's changed files
/// are embedded inline (see `commits_for_repo`), so no per-commit git process is
/// needed. Record layout: `\x1e` then eight `\x1f`-separated header fields, then
/// git's newline, then the NUL-separated name-status entries.
fn parse_commit_log_inline(output: &[u8]) -> Result<Vec<GitCommit>, String> {
const FIELD_SEPARATOR: u8 = 0x1f;
const RECORD_SEPARATOR: u8 = 0x1e;
let mut commits = Vec::new();
for record in output.split(|byte| *byte == RECORD_SEPARATOR) {
// Skip the empty leading chunk and any stray separators left by `-z`.
if record
.iter()
.all(|&byte| matches!(byte, 0 | b'\n' | b'\r' | b' ' | b'\t'))
{
continue;
}
let parts: Vec<&[u8]> = record.splitn(9, |byte| *byte == FIELD_SEPARATOR).collect();
if parts.len() < 8 {
return Err(format!(
"Unerwarteter Git-Log-Eintrag: {}",
String::from_utf8_lossy(record)
));
}
// Field 8 (if present) holds the name-status list, preceded by the newline
// git inserts between the pretty-format output and the diff.
let mut files_bytes: &[u8] = parts.get(8).copied().unwrap_or(&[]);
while let Some((&first, rest)) = files_bytes.split_first() {
if matches!(first, b'\n' | b'\r') {
files_bytes = rest;
} else {
break;
}
}
let refs = String::from_utf8_lossy(parts[5])
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.map(ToString::to_string)
.collect();
let parents = String::from_utf8_lossy(parts[6])
.split_whitespace()
.map(ToString::to_string)
.collect();
commits.push(GitCommit {
hash: String::from_utf8_lossy(parts[0]).trim().to_string(),
short_hash: String::from_utf8_lossy(parts[1]).trim().to_string(),
author_name: String::from_utf8_lossy(parts[2]).to_string(),
author_email: String::from_utf8_lossy(parts[3]).to_string(),
date: String::from_utf8_lossy(parts[4]).trim().to_string(),
refs,
parents,
summary: String::from_utf8_lossy(parts[7]).to_string(),
files: parse_commit_files(files_bytes)?,
});
}
Ok(commits)
}
fn parse_commit_log(repo: &Path, output: &[u8]) -> Result<Vec<GitCommit>, String> {
const FIELD_SEPARATOR: char = '\x1f';
const RECORD_SEPARATOR: char = '\x1e';
@@ -3234,6 +3369,7 @@ mod tests {
let status = create_branch(
repo.path.to_string_lossy().to_string(),
"feature/new-panel".to_string(),
None,
)
.unwrap();
@@ -3246,6 +3382,7 @@ mod tests {
let err = create_branch(
repo.path.to_string_lossy().to_string(),
"feature/new-panel".to_string(),
None,
)
.unwrap_err();
assert!(err.contains("existiert bereits"));
+3 -1
View File
@@ -6,7 +6,8 @@ use git::{
cancel_code_search, checkout_branch, commit, compare_commits, compare_file_to_head,
compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
diff_file_against_working_tree, get_remote_url, get_status, list_branches, list_commits,
list_file_history, list_repository_files, merge_branch, open_repository, pull, push,
list_file_history, list_repository_files, merge_branch, open_repository,
open_repository_bundle, pull, push,
read_conflict, resolve_conflict, resolve_conflict_side, restore_file_from_commit,
restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files,
SearchCancellationState,
@@ -34,6 +35,7 @@ fn main() {
restore_file_from_commit,
merge_branch,
list_repository_files,
open_repository_bundle,
list_file_history,
compare_commits,
compare_file_to_head,