add more features
This commit is contained in:
@@ -63,6 +63,25 @@ pub struct GitCommitFile {
|
||||
pub status: FileStatusKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct GitDiffFile {
|
||||
pub path: String,
|
||||
pub old_path: Option<String>,
|
||||
pub status: FileStatusKind,
|
||||
pub additions: u32,
|
||||
pub deletions: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct GitCommitComparison {
|
||||
pub from_hash: String,
|
||||
pub from_short: String,
|
||||
pub to_hash: String,
|
||||
pub to_short: String,
|
||||
pub files: Vec<GitDiffFile>,
|
||||
pub patch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct GitRepositoryFile {
|
||||
pub path: String,
|
||||
@@ -330,6 +349,96 @@ pub fn restore_file_from_commit(
|
||||
status_for_repo(&repo)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn compare_commits(
|
||||
path: String,
|
||||
from: String,
|
||||
to: String,
|
||||
) -> Result<GitCommitComparison, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let from_hash = verify_commit(&repo, &from)?;
|
||||
let to_hash = verify_commit(&repo, &to)?;
|
||||
|
||||
let name_status = run_git(
|
||||
&repo,
|
||||
[
|
||||
"diff",
|
||||
"--name-status",
|
||||
"-M",
|
||||
"-z",
|
||||
from_hash.as_str(),
|
||||
to_hash.as_str(),
|
||||
],
|
||||
)?;
|
||||
let numstat = run_git(
|
||||
&repo,
|
||||
[
|
||||
"diff",
|
||||
"--numstat",
|
||||
"-M",
|
||||
"-z",
|
||||
from_hash.as_str(),
|
||||
to_hash.as_str(),
|
||||
],
|
||||
)?;
|
||||
let patch_output = run_git(&repo, ["diff", "-M", from_hash.as_str(), to_hash.as_str()])?;
|
||||
|
||||
let files = parse_diff_files(&name_status, &numstat)?;
|
||||
let patch = String::from_utf8_lossy(&patch_output).to_string();
|
||||
|
||||
Ok(GitCommitComparison {
|
||||
from_short: short_hash(&from_hash),
|
||||
to_short: short_hash(&to_hash),
|
||||
from_hash,
|
||||
to_hash,
|
||||
files,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn diff_file_against_working_tree(
|
||||
path: String,
|
||||
commit: String,
|
||||
file: String,
|
||||
) -> Result<GitCommitComparison, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
validate_files(std::slice::from_ref(&file))?;
|
||||
let commit_hash = verify_commit(&repo, &commit)?;
|
||||
|
||||
let name_status = run_git_with_paths(
|
||||
&repo,
|
||||
&["diff", "--name-status", "-M", "-z", commit_hash.as_str()],
|
||||
std::slice::from_ref(&file),
|
||||
)?;
|
||||
let numstat = run_git_with_paths(
|
||||
&repo,
|
||||
&["diff", "--numstat", "-M", "-z", commit_hash.as_str()],
|
||||
std::slice::from_ref(&file),
|
||||
)?;
|
||||
let patch_output = run_git_with_paths(
|
||||
&repo,
|
||||
&["diff", "-M", commit_hash.as_str()],
|
||||
std::slice::from_ref(&file),
|
||||
)?;
|
||||
|
||||
let files = parse_diff_files(&name_status, &numstat)?;
|
||||
let patch = String::from_utf8_lossy(&patch_output).to_string();
|
||||
|
||||
Ok(GitCommitComparison {
|
||||
from_short: short_hash(&commit_hash),
|
||||
to_short: "working tree".to_string(),
|
||||
from_hash: commit_hash,
|
||||
to_hash: String::new(),
|
||||
files,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
|
||||
fn short_hash(hash: &str) -> String {
|
||||
hash.chars().take(7).collect()
|
||||
}
|
||||
|
||||
fn resolve_repo(path: &str) -> Result<PathBuf, String> {
|
||||
if path.trim().is_empty() {
|
||||
return Err("Repository-Pfad darf nicht leer sein.".to_string());
|
||||
@@ -609,6 +718,72 @@ fn parse_commit_files(output: &[u8]) -> Result<Vec<GitCommitFile>, String> {
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn parse_diff_files(name_status: &[u8], numstat: &[u8]) -> Result<Vec<GitDiffFile>, String> {
|
||||
let status_files = parse_commit_files(name_status)?;
|
||||
let counts = parse_numstat_z(numstat);
|
||||
|
||||
let files = status_files
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
let (additions, deletions) = counts
|
||||
.iter()
|
||||
.find(|(path, _, _)| *path == file.path)
|
||||
.map(|(_, additions, deletions)| (*additions, *deletions))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
GitDiffFile {
|
||||
path: file.path,
|
||||
old_path: file.old_path,
|
||||
status: file.status,
|
||||
additions,
|
||||
deletions,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn parse_numstat_z(output: &[u8]) -> Vec<(String, u32, u32)> {
|
||||
let tokens: Vec<String> = output
|
||||
.split(|byte| *byte == 0)
|
||||
.filter(|entry| !entry.is_empty())
|
||||
.map(|entry| String::from_utf8_lossy(entry).to_string())
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut index = 0;
|
||||
|
||||
while index < tokens.len() {
|
||||
let mut parts = tokens[index].splitn(3, '\t');
|
||||
let additions = parse_numstat_count(parts.next().unwrap_or(""));
|
||||
let deletions = parse_numstat_count(parts.next().unwrap_or(""));
|
||||
let rest = parts.next().unwrap_or("").to_string();
|
||||
index += 1;
|
||||
|
||||
// For renames/copies, `--numstat -z` leaves the path empty and emits the
|
||||
// old and new paths as two separate NUL-terminated tokens.
|
||||
let path = if rest.is_empty() {
|
||||
if index + 1 >= tokens.len() {
|
||||
break;
|
||||
}
|
||||
let new_path = tokens[index + 1].clone();
|
||||
index += 2;
|
||||
new_path
|
||||
} else {
|
||||
rest
|
||||
};
|
||||
|
||||
result.push((path, additions, deletions));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_numstat_count(value: &str) -> u32 {
|
||||
value.trim().parse().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn map_name_status(status: &str) -> FileStatusKind {
|
||||
match status.chars().next() {
|
||||
Some('M') => FileStatusKind::Modified,
|
||||
@@ -1231,6 +1406,105 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_diff_files_merges_name_status_with_numstat_counts() {
|
||||
let name_status = b"M\0src/main.rs\0A\0README.md\0R100\0old.txt\0new.txt\0";
|
||||
let numstat = b"4\t2\tsrc/main.rs\010\t0\tREADME.md\00\t0\t\0old.txt\0new.txt\0";
|
||||
|
||||
let files = parse_diff_files(name_status, numstat).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
files,
|
||||
vec![
|
||||
GitDiffFile {
|
||||
path: "src/main.rs".to_string(),
|
||||
old_path: None,
|
||||
status: FileStatusKind::Modified,
|
||||
additions: 4,
|
||||
deletions: 2,
|
||||
},
|
||||
GitDiffFile {
|
||||
path: "README.md".to_string(),
|
||||
old_path: None,
|
||||
status: FileStatusKind::Added,
|
||||
additions: 10,
|
||||
deletions: 0,
|
||||
},
|
||||
GitDiffFile {
|
||||
path: "new.txt".to_string(),
|
||||
old_path: Some("old.txt".to_string()),
|
||||
status: FileStatusKind::Renamed,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_numstat_treats_binary_dashes_as_zero() {
|
||||
let counts = parse_numstat_z(b"-\t-\tlogo.png\0");
|
||||
|
||||
assert_eq!(counts, vec![("logo.png".to_string(), 0, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_commits_reports_changes_between_two_commits() {
|
||||
let repo = init_temp_repo("compare_commits");
|
||||
commit_initial_file(&repo.path);
|
||||
let first_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
|
||||
|
||||
fs::write(repo.path.join("old.txt"), "original\nsecond line\n")
|
||||
.expect("tracked file should change");
|
||||
fs::write(repo.path.join("added.txt"), "brand new\n").expect("added file should be written");
|
||||
run_git_test(&repo.path, ["add", "old.txt", "added.txt"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "second"]);
|
||||
let second_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
|
||||
|
||||
let comparison = compare_commits(
|
||||
repo.path.to_string_lossy().to_string(),
|
||||
first_commit,
|
||||
second_commit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(comparison
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified));
|
||||
assert!(comparison
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path == "added.txt" && file.status == FileStatusKind::Added));
|
||||
assert!(comparison.patch.contains("second line"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_file_against_working_tree_reports_uncommitted_changes() {
|
||||
let repo = init_temp_repo("diff_against_working_tree");
|
||||
commit_initial_file(&repo.path);
|
||||
let first_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
|
||||
|
||||
// Change the file on disk without committing.
|
||||
fs::write(repo.path.join("old.txt"), "working tree change\n")
|
||||
.expect("working tree file should change");
|
||||
|
||||
let comparison = diff_file_against_working_tree(
|
||||
repo.path.to_string_lossy().to_string(),
|
||||
first_commit,
|
||||
"old.txt".to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(comparison.to_short, "working tree");
|
||||
assert!(comparison.to_hash.is_empty());
|
||||
assert!(comparison
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified));
|
||||
assert!(comparison.patch.contains("working tree change"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_staged_added_file_removes_it_from_index_and_worktree() {
|
||||
let repo = init_temp_repo("restore_staged_added_file");
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
mod git;
|
||||
|
||||
use git::{
|
||||
checkout_branch, commit, get_status, list_branches, list_commits, list_file_history,
|
||||
list_repository_files, merge_branch, open_repository, pull, push, restore_file_from_commit,
|
||||
restore_files, restore_to_commit, stage_files, unstage_files,
|
||||
checkout_branch, commit, compare_commits, diff_file_against_working_tree, get_status,
|
||||
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
|
||||
open_repository, pull, push, restore_file_from_commit, restore_files, restore_to_commit,
|
||||
stage_files, unstage_files,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -26,7 +27,9 @@ fn main() {
|
||||
restore_file_from_commit,
|
||||
merge_branch,
|
||||
list_repository_files,
|
||||
list_file_history
|
||||
list_file_history,
|
||||
compare_commits,
|
||||
diff_file_against_working_tree
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user