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:
+144
-7
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user