huskies: merge 682_refactor_decompose_server_src_agents_merge_squash_rs_1346_lines
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
use super::*;
|
||||
use std::process::Command;
|
||||
|
||||
fn init_git_repo(repo: &std::path::Path) {
|
||||
Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["config", "user.email", "test@test.com"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["config", "user.name", "Test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "--allow-empty", "-m", "init"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_uses_merge_queue_no_conflict_markers_on_master() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create a file that will be conflicted on master.
|
||||
fs::write(repo.join("shared.txt"), "line 1\nline 2\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "initial shared file"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Create a feature branch that modifies the file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-conflict_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(
|
||||
repo.join("shared.txt"),
|
||||
"line 1\nline 2\nfeature addition\n",
|
||||
)
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature: add line"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Switch to master and make a conflicting change.
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("shared.txt"), "line 1\nline 2\nmaster addition\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "master: add line"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Run the squash merge.
|
||||
let result = run_squash_merge(repo, "feature/story-conflict_test", "conflict_test").unwrap();
|
||||
|
||||
// Master should NEVER contain conflict markers, regardless of outcome.
|
||||
let master_content = fs::read_to_string(repo.join("shared.txt")).unwrap();
|
||||
assert!(
|
||||
!master_content.contains("<<<<<<<"),
|
||||
"master must never contain conflict markers, got:\n{master_content}"
|
||||
);
|
||||
assert!(
|
||||
!master_content.contains(">>>>>>>"),
|
||||
"master must never contain conflict markers, got:\n{master_content}"
|
||||
);
|
||||
|
||||
// The merge should have had conflicts.
|
||||
assert!(result.had_conflicts, "should detect conflicts");
|
||||
|
||||
// Conflicts should have been auto-resolved (both are simple additions).
|
||||
if result.conflicts_resolved {
|
||||
assert!(result.success, "auto-resolved merge should succeed");
|
||||
assert!(
|
||||
master_content.contains("master addition"),
|
||||
"master side should be present"
|
||||
);
|
||||
assert!(
|
||||
master_content.contains("feature addition"),
|
||||
"feature side should be present"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify no leftover merge-queue branch.
|
||||
let branches = Command::new("git")
|
||||
.args(["branch", "--list", "merge-queue/*"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let branch_list = String::from_utf8_lossy(&branches.stdout);
|
||||
assert!(
|
||||
branch_list.trim().is_empty(),
|
||||
"merge-queue branch should be cleaned up, got: {branch_list}"
|
||||
);
|
||||
|
||||
// Verify no leftover merge workspace directory.
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_clean_merge_succeeds() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create feature branch with a new file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-clean_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("new_file.txt"), "new content").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add new file"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Switch back to master.
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-clean_test", "clean_test").unwrap();
|
||||
|
||||
assert!(result.success, "clean merge should succeed");
|
||||
assert!(
|
||||
!result.had_conflicts,
|
||||
"clean merge should have no conflicts"
|
||||
);
|
||||
assert!(
|
||||
!result.conflicts_resolved,
|
||||
"no conflicts means nothing to resolve"
|
||||
);
|
||||
assert!(
|
||||
repo.join("new_file.txt").exists(),
|
||||
"merged file should exist on master"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_nonexistent_branch_fails() {
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-nope", "nope").unwrap();
|
||||
|
||||
assert!(!result.success, "merge of nonexistent branch should fail");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_succeeds_when_master_diverges() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create an initial file on master.
|
||||
fs::write(repo.join("base.txt"), "base content\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "initial"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Create a feature branch with a new file (clean merge, no conflicts).
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-diverge_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("feature.txt"), "feature content\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature: add file"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Switch back to master and simulate a filesystem watcher commit
|
||||
// (e.g. a pipeline file move) that advances master beyond the point
|
||||
// where the merge-queue branch will be created.
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("diverge_test.md"), "---\nname: test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "huskies: queue diverge_test for merge"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Run the squash merge. With the old fast-forward approach, this
|
||||
// would fail because master diverged. With cherry-pick, it succeeds.
|
||||
let result = run_squash_merge(repo, "feature/story-diverge_test", "diverge_test").unwrap();
|
||||
|
||||
assert!(
|
||||
result.success,
|
||||
"squash merge should succeed despite diverged master: {}",
|
||||
result.output
|
||||
);
|
||||
assert!(!result.had_conflicts, "no conflicts expected");
|
||||
|
||||
// Verify the feature file landed on master.
|
||||
assert!(
|
||||
repo.join("feature.txt").exists(),
|
||||
"feature file should be on master after cherry-pick"
|
||||
);
|
||||
let feature_content = fs::read_to_string(repo.join("feature.txt")).unwrap();
|
||||
assert_eq!(feature_content, "feature content\n");
|
||||
|
||||
// Verify the watcher commit's file is still present.
|
||||
assert!(
|
||||
sk_dir.join("diverge_test.md").exists(),
|
||||
"watcher-committed file should still be on master"
|
||||
);
|
||||
|
||||
// Verify cleanup: no merge-queue branch, no merge workspace.
|
||||
let branches = Command::new("git")
|
||||
.args(["branch", "--list", "merge-queue/*"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let branch_list = String::from_utf8_lossy(&branches.stdout);
|
||||
assert!(
|
||||
branch_list.trim().is_empty(),
|
||||
"merge-queue branch should be cleaned up, got: {branch_list}"
|
||||
);
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_empty_diff_fails() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create a file on master.
|
||||
fs::write(repo.join("code.txt"), "content\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add code"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Create a feature branch with NO additional changes (empty diff).
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-empty_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-empty_test", "empty_test");
|
||||
|
||||
// Bug 226 / 675: a zero-commit branch must not be treated as success.
|
||||
// The pre-flight check (bug 675) returns Err for zero commits ahead;
|
||||
// the older code path returned Ok(SquashMergeResult { success: false }).
|
||||
// Either form is a failure — just not success.
|
||||
match result {
|
||||
Ok(r) => assert!(
|
||||
!r.success,
|
||||
"empty diff merge must fail, not silently succeed: {}",
|
||||
r.output
|
||||
),
|
||||
Err(e) => assert!(
|
||||
e.contains("no commits to merge") || e.contains("nothing to commit"),
|
||||
"unexpected error: {e}"
|
||||
),
|
||||
}
|
||||
|
||||
// Cleanup should still happen (no workspace was created for the Err path).
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user