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 {
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"]);
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);
}