add something

This commit is contained in:
Christoph Brandau
2026-06-29 14:40:42 +02:00
parent a15d6c0b2c
commit 9861fa2446
12 changed files with 822 additions and 42 deletions
+110 -12
View File
@@ -117,6 +117,8 @@ enum CheckoutPlan {
Raw(String),
}
const FULL_FILE_DIFF_CONTEXT: &str = "--unified=1000000";
#[tauri::command]
pub fn open_repository(path: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
@@ -247,6 +249,13 @@ pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
return Err("Commit-Message darf nicht leer sein.".to_string());
}
let current_status = status_for_repo(&repo)?;
if has_unresolved_conflicts(&current_status) {
return Err(
"Merge-Konflikte muessen geloest werden, bevor du committen kannst.".to_string(),
);
}
run_git(&repo, ["commit", "-m", message.as_str()])?;
status_for_repo(&repo)
}
@@ -310,10 +319,7 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
// Surface those through the status so the UI can offer conflict resolution
// instead of treating the conflict as a hard error.
let status = status_for_repo(&repo)?;
if status.files.iter().any(|file| {
matches!(file.staged, Some(FileStatusKind::Conflicted))
|| matches!(file.unstaged, Some(FileStatusKind::Conflicted))
}) {
if has_unresolved_conflicts(&status) {
return Ok(status);
}
@@ -447,7 +453,16 @@ pub fn compare_commits(
to_hash.as_str(),
],
)?;
let patch_output = run_git(&repo, ["diff", "-M", from_hash.as_str(), to_hash.as_str()])?;
let patch_output = run_git(
&repo,
[
"diff",
"-M",
FULL_FILE_DIFF_CONTEXT,
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();
@@ -484,7 +499,7 @@ pub fn diff_file_against_working_tree(
)?;
let patch_output = run_git_with_paths(
&repo,
&["diff", "-M", commit_hash.as_str()],
&["diff", "-M", FULL_FILE_DIFF_CONTEXT, commit_hash.as_str()],
std::slice::from_ref(&file),
)?;
@@ -748,6 +763,13 @@ fn status_for_file(statuses: &[GitFileStatus], path: &str) -> Option<FileStatusK
status.unstaged.or(status.staged)
}
fn has_unresolved_conflicts(status: &GitStatus) -> bool {
status.files.iter().any(|file| {
matches!(file.staged, Some(FileStatusKind::Conflicted))
|| matches!(file.unstaged, Some(FileStatusKind::Conflicted))
})
}
fn parse_commit_log(repo: &Path, output: &[u8]) -> Result<Vec<GitCommit>, String> {
const FIELD_SEPARATOR: char = '\x1f';
const RECORD_SEPARATOR: char = '\x1e';
@@ -1713,7 +1735,8 @@ mod tests {
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");
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"]);
@@ -1736,6 +1759,40 @@ mod tests {
assert!(comparison.patch.contains("second line"));
}
#[test]
fn compare_commits_includes_full_file_context() {
let repo = init_temp_repo("compare_full_context");
let before = (1..=60)
.map(|line| format!("line {line}\n"))
.collect::<String>();
fs::write(repo.path.join("context.txt"), &before).expect("context file should be written");
run_git_test(&repo.path, ["add", "context.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "base"]);
let first_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
fs::write(
repo.path.join("context.txt"),
before.replace("line 30\n", "line 30 changed\n"),
)
.expect("context file should be changed");
run_git_test(&repo.path, ["add", "context.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "change"]);
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.patch.contains(" line 1\n"));
assert!(comparison.patch.contains(" line 60\n"));
assert!(comparison.patch.contains("-line 30\n"));
assert!(comparison.patch.contains("+line 30 changed\n"));
}
#[test]
fn diff_file_against_working_tree_reports_uncommitted_changes() {
let repo = init_temp_repo("diff_against_working_tree");
@@ -1786,8 +1843,11 @@ mod tests {
.output()
.expect("git merge should start");
let conflict =
read_conflict(repo.path.to_string_lossy().to_string(), "file.txt".to_string()).unwrap();
let conflict = read_conflict(
repo.path.to_string_lossy().to_string(),
"file.txt".to_string(),
)
.unwrap();
assert_eq!(
conflict.ours.unwrap().replace("\r\n", "\n"),
"ours change\n"
@@ -1813,6 +1873,40 @@ mod tests {
assert_eq!(contents.replace("\r\n", "\n"), "resolved\n");
}
#[test]
fn commit_rejects_unresolved_merge_conflicts() {
let repo = init_temp_repo("commit_rejects_conflicts");
fs::write(repo.path.join("file.txt"), "base\n").expect("base file should be written");
run_git_test(&repo.path, ["add", "file.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "base"]);
let main_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
run_git_test(&repo.path, ["checkout", "-q", "-b", "feature"]);
fs::write(repo.path.join("file.txt"), "theirs change\n").expect("feature change");
run_git_test(&repo.path, ["commit", "-q", "-am", "feature change"]);
run_git_test(&repo.path, ["checkout", "-q", main_branch.as_str()]);
fs::write(repo.path.join("file.txt"), "ours change\n").expect("main change");
run_git_test(&repo.path, ["commit", "-q", "-am", "main change"]);
let _ = Command::new("git")
.arg("-C")
.arg(&repo.path)
.args(["merge", "--no-edit", "feature"])
.output()
.expect("git merge should start");
let err = commit(
repo.path.to_string_lossy().to_string(),
"should not commit".to_string(),
)
.unwrap_err();
assert!(err.contains("Merge-Konflikte"));
let status = status_for_repo(&repo.path).unwrap();
assert!(has_unresolved_conflicts(&status));
}
#[test]
fn detects_binary_content_by_nul_byte() {
assert!(is_binary_bytes(&[0u8, 1, 2, 3]));
@@ -1822,7 +1916,8 @@ mod tests {
#[test]
fn binary_conflict_can_be_resolved_by_side() {
let repo = init_temp_repo("binary_conflict");
fs::write(repo.path.join("img.bin"), [0u8, 1, 2, 3]).expect("base binary should be written");
fs::write(repo.path.join("img.bin"), [0u8, 1, 2, 3])
.expect("base binary should be written");
run_git_test(&repo.path, ["add", "img.bin"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "base"]);
let main_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -1842,8 +1937,11 @@ mod tests {
.output()
.expect("git merge should start");
let conflict =
read_conflict(repo.path.to_string_lossy().to_string(), "img.bin".to_string()).unwrap();
let conflict = read_conflict(
repo.path.to_string_lossy().to_string(),
"img.bin".to_string(),
)
.unwrap();
assert!(conflict.binary);
assert!(conflict.content.is_empty());
assert_eq!(conflict.ours_size, Some(3));