use super::commit::{git_text, remove_settled};
use super::service_commit::{Running, start, through_service};
use super::support::{Fixture, fixture, git, repository};
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::Output;
fn unfinished_merge(fixture: &Fixture, name: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
git(fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
for file in ["x.txt", "y.txt", "z.txt"] {
std::fs::write(repo.join(file), "base\n").expect("file");
}
git(fixture, &repo, &["add", "--", "x.txt", "y.txt", "z.txt"]);
git(fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(fixture, &repo, &["switch", "--quiet", "--create", "other"]);
std::fs::write(repo.join("y.txt"), "other\n").expect("file");
git(fixture, &repo, &["commit", "--quiet", "--all", "--message=other"]);
git(fixture, &repo, &["switch", "--quiet", "main"]);
std::fs::write(repo.join("z.txt"), "main\n").expect("file");
git(fixture, &repo, &["commit", "--quiet", "--all", "--message=main"]);
git(fixture, &repo, &["merge", "--no-commit", "--no-ff", "other"]);
return repo;
}
#[test]
fn a_path_commit_during_an_unfinished_merge_lands_and_the_merge_is_carried_forward() {
let fixture: Fixture = fixture("commit-during-merge");
let repo: PathBuf = unfinished_merge(&fixture, "repo");
let name: String = format!("cdm-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let head_before: String = git_text(&fixture, &repo, &["rev-parse", "HEAD"]).trim().to_owned();
let staged_before: String = git_text(&fixture, &repo, &["ls-files", "--stage", "--", "y.txt"]);
std::fs::write(repo.join("x.txt"), "edited by an unrelated session\n").expect("file");
let served: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=unrelated", "--", "x.txt"]);
assert_eq!(served.status.code(), Some(0), "{}", String::from_utf8_lossy(&served.stderr));
let head_after: String = git_text(&fixture, &repo, &["rev-parse", "HEAD"]).trim().to_owned();
assert_ne!(head_after, head_before, "the commit landed");
assert_eq!(git_text(&fixture, &repo, &["rev-parse", "HEAD^"]).trim(), head_before);
assert_eq!(git_text(&fixture, &repo, &["diff-tree", "-r", "--name-only", "HEAD^", "HEAD"]).trim(), "x.txt", "only its path");
assert!(repo.join(".git/MERGE_HEAD").exists(), "the merge is carried forward");
assert_eq!(git_text(&fixture, &repo, &["ls-files", "--stage", "--", "y.txt"]), staged_before, "the merge result stays staged");
let concluded: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=conclude the merge"]);
assert_eq!(concluded.status.code(), Some(0), "{}", String::from_utf8_lossy(&concluded.stderr));
let parents: Vec<String> = git_text(&fixture, &repo, &["rev-list", "--parents", "-n", "1", "HEAD"]).split_whitespace().skip(1).map(String::from).collect();
assert_eq!(parents.len(), 2, "the merge commit");
assert_eq!(parents[0], head_after, "it sits on the head that moved");
assert_eq!(parents[1], git_text(&fixture, &repo, &["rev-parse", "other"]).trim());
assert!(!repo.join(".git/MERGE_HEAD").exists());
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
#[test]
fn during_a_conflicted_merge_an_unrelated_path_lands_and_the_conflict_is_carried_on() {
let fixture: Fixture = fixture("commit-during-conflict");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
git(&fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
for file in ["x.txt", "y.txt"] {
std::fs::write(repo.join(file), "base\n").expect("file");
}
git(&fixture, &repo, &["add", "--", "x.txt", "y.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(&fixture, &repo, &["switch", "--quiet", "--create", "other"]);
std::fs::write(repo.join("y.txt"), "other\n").expect("file");
git(&fixture, &repo, &["commit", "--quiet", "--all", "--message=other"]);
git(&fixture, &repo, &["switch", "--quiet", "main"]);
std::fs::write(repo.join("y.txt"), "main\n").expect("file");
git(&fixture, &repo, &["commit", "--quiet", "--all", "--message=main"]);
let merged = super::support::direct(&fixture).current_dir(&repo).args(["merge", "--no-edit", "other"]).output().expect("git runs");
assert_eq!(merged.status.code(), Some(1), "the merge conflicts");
let name: String = format!("cdc-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
std::fs::write(repo.join("x.txt"), "edited by an unrelated session\n").expect("file");
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=unrelated", "--", "x.txt"]);
assert_eq!(landed.status.code(), Some(0), "{}", String::from_utf8_lossy(&landed.stderr));
assert_eq!(git_text(&fixture, &repo, &["diff-tree", "-r", "--name-only", "HEAD^", "HEAD"]).trim(), "x.txt");
assert!(repo.join(".git/MERGE_HEAD").exists(), "the conflicted merge is carried on");
assert!(git_text(&fixture, &repo, &["ls-files", "--unmerged"]).contains("y.txt"), "the conflict is still the merge's to resolve");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
#[test]
fn a_commit_naming_a_conflicted_path_lands_with_its_markers_and_warns() {
let fixture: Fixture = fixture("commit-conflicted-path");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
git(&fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
std::fs::write(repo.join("y.txt"), "base\n").expect("file");
git(&fixture, &repo, &["add", "--", "y.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(&fixture, &repo, &["switch", "--quiet", "--create", "other"]);
std::fs::write(repo.join("y.txt"), "other\n").expect("file");
git(&fixture, &repo, &["commit", "--quiet", "--all", "--message=other"]);
git(&fixture, &repo, &["switch", "--quiet", "main"]);
std::fs::write(repo.join("y.txt"), "main\n").expect("file");
git(&fixture, &repo, &["commit", "--quiet", "--all", "--message=main"]);
let merged = super::support::direct(&fixture).current_dir(&repo).args(["merge", "--no-edit", "other"]).output().expect("git runs");
assert_eq!(merged.status.code(), Some(1), "the merge conflicts");
let name: String = format!("ccp-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let before: String = git_text(&fixture, &repo, &["rev-parse", "HEAD"]).trim().to_owned();
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=conflicted on purpose", "--", "y.txt"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "a conflicted path is committed: {stderr}");
assert_ne!(git_text(&fixture, &repo, &["rev-parse", "HEAD"]).trim(), before, "a commit landed");
assert!(git_text(&fixture, &repo, &["show", "HEAD:y.txt"]).contains("<<<<<<<"), "the committed blob holds the markers");
assert!(stderr.contains("y.txt") && stderr.contains("conflict markers") && stderr.contains("committed"), "the warning names the path: {stderr}");
assert!(stderr.contains("follow-up commit"), "the warning says how to fix it: {stderr}");
assert!(repo.join(".git/MERGE_HEAD").exists(), "the merge is carried on");
assert_eq!(git_text(&fixture, &repo, &["ls-files", "--unmerged"]), "", "the committed path is no longer unmerged in the index");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
fn conflicted_merge(fixture: &Fixture, name: &str) -> PathBuf {
return conflicted_merge_with_attributes(fixture, name, None);
}
fn conflicted_merge_with_attributes(fixture: &Fixture, name: &str, attributes: Option<&str>) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
git(fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
std::fs::write(repo.join("y.txt"), "base\n").expect("file");
git(fixture, &repo, &["add", "--", "y.txt"]);
if let Some(lines) = attributes {
std::fs::write(repo.join(".gitattributes"), lines).expect("attributes");
git(fixture, &repo, &["add", "--", ".gitattributes"]);
}
git(fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(fixture, &repo, &["switch", "--quiet", "--create", "other"]);
std::fs::write(repo.join("y.txt"), "other\n").expect("file");
git(fixture, &repo, &["commit", "--quiet", "--all", "--message=other"]);
git(fixture, &repo, &["switch", "--quiet", "main"]);
std::fs::write(repo.join("y.txt"), "main\n").expect("file");
git(fixture, &repo, &["commit", "--quiet", "--all", "--message=main"]);
let merged = super::support::direct(fixture).current_dir(&repo).args(["merge", "--no-edit", "other"]).output().expect("git runs");
assert_eq!(merged.status.code(), Some(1), "the merge conflicts");
return repo;
}
#[test]
fn a_plain_commit_with_unmerged_files_finishes_the_merge_with_their_markers_and_warns() {
let fixture: Fixture = fixture("plain-commit-conflicted");
let repo: PathBuf = conflicted_merge(&fixture, "repo");
let name: String = format!("pcc-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=finish the merge"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "the merge is finished: {stderr}");
assert_eq!(git_text(&fixture, &repo, &["rev-list", "--parents", "--max-count=1", "HEAD"]).split_whitespace().count(), 3, "a merge commit: one commit and two parents");
assert!(git_text(&fixture, &repo, &["show", "HEAD:y.txt"]).contains("<<<<<<<"), "the committed blob holds the markers");
assert!(!repo.join(".git/MERGE_HEAD").exists(), "the merge is concluded");
assert_eq!(git_text(&fixture, &repo, &["ls-files", "--unmerged"]), "", "nothing is unmerged any more");
assert!(stderr.contains("y.txt") && stderr.contains("conflict markers") && stderr.contains("committed"), "the warning names the file: {stderr}");
assert!(stderr.contains("follow-up commit"), "the warning says how to fix it: {stderr}");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
#[test]
fn a_commit_with_all_over_unmerged_files_commits_their_markers_and_warns() {
let fixture: Fixture = fixture("all-commit-conflicted");
let repo: PathBuf = conflicted_merge(&fixture, "repo");
let name: String = format!("acc-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--no-enforce-only", "-a", "--message=finish the merge with -a"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "the merge is finished: {stderr}");
assert!(git_text(&fixture, &repo, &["show", "HEAD:y.txt"]).contains("<<<<<<<"), "the committed blob holds the markers");
assert!(!repo.join(".git/MERGE_HEAD").exists(), "the merge is concluded");
assert!(stderr.contains("y.txt") && stderr.contains("conflict markers") && stderr.contains("committed"), "the warning names the file: {stderr}");
assert!(stderr.contains("follow-up commit"), "the warning says how to fix it: {stderr}");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
fn says_the_merge_is_open(stderr: &str) -> bool {
return stderr.contains("merge is still in progress") && stderr.contains("git merge --continue") && stderr.contains("git commit");
}
#[test]
fn a_resolved_but_unstaged_path_committed_by_name_makes_no_marker_claim_and_says_how_to_finish() {
let fixture: Fixture = fixture("commit-resolved-unstaged");
let repo: PathBuf = conflicted_merge(&fixture, "repo");
std::fs::write(repo.join("y.txt"), "resolved by hand\n").expect("the resolution");
let name: String = format!("cru-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=resolve y", "--", "y.txt"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "{stderr}");
assert_eq!(git_text(&fixture, &repo, &["show", "HEAD:y.txt"]), "resolved by hand\n", "the committed blob is the resolution");
assert!(repo.join(".git/MERGE_HEAD").exists(), "the merge is carried on");
assert!(!stderr.contains("conflict markers"), "no marker is claimed for a clean blob: {stderr}");
assert!(says_the_merge_is_open(stderr.as_str()), "the merge is said to be open, with the commands that finish it: {stderr}");
let finished: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--no-edit"]);
assert_eq!(finished.status.code(), Some(0), "the line's remedy finishes the merge: {}", String::from_utf8_lossy(&finished.stderr));
assert!(!repo.join(".git/MERGE_HEAD").exists(), "the merge is concluded");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
#[test]
fn a_path_committed_by_name_with_marker_lines_keeps_the_claim_and_says_the_merge_is_open() {
let fixture: Fixture = fixture("commit-markers-open");
let repo: PathBuf = conflicted_merge(&fixture, "repo");
let name: String = format!("cmo-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=markers on purpose", "--", "y.txt"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "{stderr}");
assert!(stderr.contains("y.txt still has conflict markers, and they were committed"), "{stderr}");
assert!(stderr.contains("follow-up commit"), "the fix of the file is said: {stderr}");
assert!(says_the_merge_is_open(stderr.as_str()), "{stderr}");
assert!(!stderr.contains("The merge in progress is not finished by this commit. To fix it"), "the follow-up commit is not offered as the way to finish the merge: {stderr}");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
#[test]
fn the_claim_follows_the_conflict_marker_size_attribute() {
for (label, rewrite) in [("nine", false), ("seven", true)] {
let fixture: Fixture = fixture(format!("commit-marker-size-{label}").as_str());
let repo: PathBuf = conflicted_merge_with_attributes(&fixture, "repo", Some("y.txt conflict-marker-size=9\n"));
assert!(std::fs::read_to_string(repo.join("y.txt")).expect("file").contains("<<<<<<<<< "), "Git wrote markers of the size the attribute asks for");
if rewrite {
std::fs::write(repo.join("y.txt"), "text about merges\n<<<<<<< is how a marker starts\n=======\n>>>>>>> is how it ends\n").expect("file");
}
let name: String = format!("cms-{label}-{}", std::process::id());
let mut service: Running = start(&fixture, name.as_str());
let landed: Output = through_service(&fixture, &repo, name.as_str(), &["commit", "--message=by name", "--", "y.txt"]);
let stderr: String = String::from_utf8_lossy(&landed.stderr).into_owned();
assert_eq!(landed.status.code(), Some(0), "{label}: {stderr}");
assert_eq!(stderr.contains("y.txt still has conflict markers"), !rewrite, "{label}: the claim follows the size the attribute gives: {stderr}");
let _killed = service.kill();
let _waited = service.wait();
remove_settled(&fixture);
}
}