The great storkit name conversion
This commit is contained in:
@@ -88,9 +88,7 @@ pub(crate) fn run_squash_merge(
|
||||
|
||||
let mut all_output = String::new();
|
||||
let merge_branch = format!("merge-queue/{story_id}");
|
||||
let merge_wt_path = project_root
|
||||
.join(".storkit")
|
||||
.join("merge_workspace");
|
||||
let merge_wt_path = project_root.join(".storkit").join("merge_workspace");
|
||||
|
||||
// Ensure we start clean: remove any leftover merge workspace.
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
@@ -153,21 +151,15 @@ pub(crate) fn run_squash_merge(
|
||||
all_output.push_str(&resolution_log);
|
||||
if resolved {
|
||||
conflicts_resolved = true;
|
||||
all_output
|
||||
.push_str("=== All conflicts resolved automatically ===\n");
|
||||
all_output.push_str("=== All conflicts resolved automatically ===\n");
|
||||
} else {
|
||||
// Could not resolve — abort, clean up, and report.
|
||||
let details = format!(
|
||||
"Merge conflicts in branch '{branch}':\n{merge_stdout}{merge_stderr}\n{resolution_log}"
|
||||
);
|
||||
conflict_details = Some(details);
|
||||
all_output
|
||||
.push_str("=== Unresolvable conflicts, aborting merge ===\n");
|
||||
cleanup_merge_workspace(
|
||||
project_root,
|
||||
&merge_wt_path,
|
||||
&merge_branch,
|
||||
);
|
||||
all_output.push_str("=== Unresolvable conflicts, aborting merge ===\n");
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
return Ok(SquashMergeResult {
|
||||
success: false,
|
||||
had_conflicts: true,
|
||||
@@ -180,11 +172,7 @@ pub(crate) fn run_squash_merge(
|
||||
}
|
||||
Err(e) => {
|
||||
all_output.push_str(&format!("Auto-resolution error: {e}\n"));
|
||||
cleanup_merge_workspace(
|
||||
project_root,
|
||||
&merge_wt_path,
|
||||
&merge_branch,
|
||||
);
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
return Ok(SquashMergeResult {
|
||||
success: false,
|
||||
had_conflicts: true,
|
||||
@@ -201,7 +189,7 @@ pub(crate) fn run_squash_merge(
|
||||
|
||||
// ── Commit in the temporary worktree ──────────────────────────
|
||||
all_output.push_str("=== git commit ===\n");
|
||||
let commit_msg = format!("story-kit: merge {story_id}");
|
||||
let commit_msg = format!("storkit: merge {story_id}");
|
||||
let commit = Command::new("git")
|
||||
.args(["commit", "-m", &commit_msg])
|
||||
.current_dir(&merge_wt_path)
|
||||
@@ -259,9 +247,7 @@ pub(crate) fn run_squash_merge(
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check merge diff: {e}"))?;
|
||||
let changed_files = String::from_utf8_lossy(&diff_check.stdout);
|
||||
let has_code_changes = changed_files
|
||||
.lines()
|
||||
.any(|f| !f.starts_with(".storkit/"));
|
||||
let has_code_changes = changed_files.lines().any(|f| !f.starts_with(".storkit/"));
|
||||
if !has_code_changes {
|
||||
all_output.push_str(
|
||||
"=== Merge commit contains only .storkit/ file moves, no code changes ===\n",
|
||||
@@ -330,8 +316,9 @@ pub(crate) fn run_squash_merge(
|
||||
Ok((false, gate_out)) => {
|
||||
all_output.push_str(&gate_out);
|
||||
all_output.push('\n');
|
||||
all_output
|
||||
.push_str("=== Quality gates FAILED — aborting fast-forward, master unchanged ===\n");
|
||||
all_output.push_str(
|
||||
"=== Quality gates FAILED — aborting fast-forward, master unchanged ===\n",
|
||||
);
|
||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||
return Ok(SquashMergeResult {
|
||||
success: false,
|
||||
@@ -451,18 +438,14 @@ fn try_resolve_conflicts(worktree: &Path) -> Result<(bool, String), String> {
|
||||
.map_err(|e| format!("Failed to list conflicted files: {e}"))?;
|
||||
|
||||
let file_list = String::from_utf8_lossy(&ls.stdout);
|
||||
let conflicted_files: Vec<&str> =
|
||||
file_list.lines().filter(|l| !l.is_empty()).collect();
|
||||
let conflicted_files: Vec<&str> = file_list.lines().filter(|l| !l.is_empty()).collect();
|
||||
|
||||
if conflicted_files.is_empty() {
|
||||
log.push_str("No conflicted files found (conflict may be index-only).\n");
|
||||
return Ok((false, log));
|
||||
}
|
||||
|
||||
log.push_str(&format!(
|
||||
"Conflicted files ({}):\n",
|
||||
conflicted_files.len()
|
||||
));
|
||||
log.push_str(&format!("Conflicted files ({}):\n", conflicted_files.len()));
|
||||
for f in &conflicted_files {
|
||||
log.push_str(&format!(" - {f}\n"));
|
||||
}
|
||||
@@ -480,9 +463,7 @@ fn try_resolve_conflicts(worktree: &Path) -> Result<(bool, String), String> {
|
||||
resolutions.push((file, resolved));
|
||||
}
|
||||
None => {
|
||||
log.push_str(&format!(
|
||||
" [COMPLEX — cannot auto-resolve] {file}\n"
|
||||
));
|
||||
log.push_str(&format!(" [COMPLEX — cannot auto-resolve] {file}\n"));
|
||||
return Ok((false, log));
|
||||
}
|
||||
}
|
||||
@@ -716,10 +697,7 @@ after
|
||||
// Ours comes before theirs
|
||||
let ours_pos = result.find("ours line 1").unwrap();
|
||||
let theirs_pos = result.find("theirs line 1").unwrap();
|
||||
assert!(
|
||||
ours_pos < theirs_pos,
|
||||
"ours should come before theirs"
|
||||
);
|
||||
assert!(ours_pos < theirs_pos, "ours should come before theirs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -758,7 +736,10 @@ ours
|
||||
>>>>>>> feature
|
||||
";
|
||||
let result = resolve_simple_conflicts(input);
|
||||
assert!(result.is_none(), "malformed conflict (no separator) should return None");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"malformed conflict (no separator) should return None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -770,14 +751,20 @@ ours
|
||||
theirs
|
||||
";
|
||||
let result = resolve_simple_conflicts(input);
|
||||
assert!(result.is_none(), "malformed conflict (no end marker) should return None");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"malformed conflict (no end marker) should return None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_simple_conflicts_preserves_no_trailing_newline() {
|
||||
let input = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> branch\nafter";
|
||||
let result = resolve_simple_conflicts(input).unwrap();
|
||||
assert!(!result.ends_with('\n'), "should not add trailing newline if original lacks one");
|
||||
assert!(
|
||||
!result.ends_with('\n'),
|
||||
"should not add trailing newline if original lacks one"
|
||||
);
|
||||
assert!(result.ends_with("after"));
|
||||
}
|
||||
|
||||
@@ -801,10 +788,22 @@ fn feature_fn() { println!(\"from feature\"); }\n\
|
||||
assert!(!result.contains("<<<<<<<"), "no conflict markers in output");
|
||||
assert!(!result.contains(">>>>>>>"), "no conflict markers in output");
|
||||
assert!(!result.contains("======="), "no separator in output");
|
||||
assert!(result.contains("fn master_fn()"), "master (ours) side must be preserved");
|
||||
assert!(result.contains("fn feature_fn()"), "feature (theirs) side must be preserved");
|
||||
assert!(result.contains("// shared code"), "context before conflict preserved");
|
||||
assert!(result.contains("// end"), "context after conflict preserved");
|
||||
assert!(
|
||||
result.contains("fn master_fn()"),
|
||||
"master (ours) side must be preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("fn feature_fn()"),
|
||||
"feature (theirs) side must be preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("// shared code"),
|
||||
"context before conflict preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("// end"),
|
||||
"context after conflict preserved"
|
||||
);
|
||||
// ours (master) must appear before theirs (feature)
|
||||
assert!(
|
||||
result.find("master_fn").unwrap() < result.find("feature_fn").unwrap(),
|
||||
@@ -830,12 +829,27 @@ export function featureImpl() {}\n\
|
||||
>>>>>>> feature/story-43\n";
|
||||
let result = resolve_simple_conflicts(input).unwrap();
|
||||
assert!(!result.contains("<<<<<<<"), "no conflict markers in output");
|
||||
assert!(result.contains("import { A }"), "first block ours preserved");
|
||||
assert!(result.contains("import { B }"), "first block theirs preserved");
|
||||
assert!(
|
||||
result.contains("import { A }"),
|
||||
"first block ours preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("import { B }"),
|
||||
"first block theirs preserved"
|
||||
);
|
||||
assert!(result.contains("masterImpl"), "second block ours preserved");
|
||||
assert!(result.contains("featureImpl"), "second block theirs preserved");
|
||||
assert!(result.contains("// imports"), "surrounding context preserved");
|
||||
assert!(result.contains("// implementation"), "surrounding context preserved");
|
||||
assert!(
|
||||
result.contains("featureImpl"),
|
||||
"second block theirs preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("// imports"),
|
||||
"surrounding context preserved"
|
||||
);
|
||||
assert!(
|
||||
result.contains("// implementation"),
|
||||
"surrounding context preserved"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -850,7 +864,10 @@ feature_addition\n\
|
||||
after\n";
|
||||
let result = resolve_simple_conflicts(input).unwrap();
|
||||
assert!(!result.contains("<<<<<<<"), "no conflict markers");
|
||||
assert!(result.contains("feature_addition"), "non-empty side preserved");
|
||||
assert!(
|
||||
result.contains("feature_addition"),
|
||||
"non-empty side preserved"
|
||||
);
|
||||
assert!(result.contains("before"), "context preserved");
|
||||
assert!(result.contains("after"), "context preserved");
|
||||
}
|
||||
@@ -885,7 +902,11 @@ after\n";
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("shared.txt"), "line 1\nline 2\nfeature addition\n").unwrap();
|
||||
fs::write(
|
||||
repo.join("shared.txt"),
|
||||
"line 1\nline 2\nfeature addition\n",
|
||||
)
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
@@ -916,8 +937,8 @@ after\n";
|
||||
.unwrap();
|
||||
|
||||
// Run the squash merge.
|
||||
let result = run_squash_merge(repo, "feature/story-conflict_test", "conflict_test")
|
||||
.unwrap();
|
||||
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();
|
||||
@@ -999,12 +1020,17 @@ after\n";
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-clean_test", "clean_test")
|
||||
.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!(
|
||||
!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"
|
||||
@@ -1019,8 +1045,7 @@ after\n";
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-nope", "nope")
|
||||
.unwrap();
|
||||
let result = run_squash_merge(repo, "feature/story-nope", "nope").unwrap();
|
||||
|
||||
assert!(!result.success, "merge of nonexistent branch should fail");
|
||||
}
|
||||
@@ -1078,36 +1103,28 @@ after\n";
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".storkit/work/4_merge");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("diverge_test.md"),
|
||||
"---\nname: test\n---\n",
|
||||
)
|
||||
.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", "story-kit: queue diverge_test for merge"])
|
||||
.args(["commit", "-m", "storkit: 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();
|
||||
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"
|
||||
);
|
||||
assert!(!result.had_conflicts, "no conflicts expected");
|
||||
|
||||
// Verify the feature file landed on master.
|
||||
assert!(
|
||||
@@ -1176,8 +1193,7 @@ after\n";
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-empty_test", "empty_test").unwrap();
|
||||
let result = run_squash_merge(repo, "feature/story-empty_test", "empty_test").unwrap();
|
||||
|
||||
// Bug 226: empty diff must NOT be treated as success.
|
||||
assert!(
|
||||
@@ -1212,11 +1228,7 @@ after\n";
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".storkit/work/2_current");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("md_only_test.md"),
|
||||
"---\nname: Test\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(sk_dir.join("md_only_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
@@ -1233,8 +1245,7 @@ after\n";
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-md_only_test", "md_only_test").unwrap();
|
||||
let result = run_squash_merge(repo, "feature/story-md_only_test", "md_only_test").unwrap();
|
||||
|
||||
// The squash merge will commit the .storkit/ file, but should fail because
|
||||
// there are no code changes outside .storkit/.
|
||||
@@ -1323,8 +1334,7 @@ after\n";
|
||||
.unwrap();
|
||||
|
||||
// Squash-merge the feature branch — conflicts because both appended to the same location.
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-238_additive", "238_additive").unwrap();
|
||||
let result = run_squash_merge(repo, "feature/story-238_additive", "238_additive").unwrap();
|
||||
|
||||
// Conflict must be detected and auto-resolved.
|
||||
assert!(result.had_conflicts, "additive conflict should be detected");
|
||||
@@ -1336,8 +1346,14 @@ after\n";
|
||||
|
||||
// Master must contain both additions without conflict markers.
|
||||
let content = fs::read_to_string(repo.join("module.rs")).unwrap();
|
||||
assert!(!content.contains("<<<<<<<"), "master must not contain conflict markers");
|
||||
assert!(!content.contains(">>>>>>>"), "master must not contain conflict markers");
|
||||
assert!(
|
||||
!content.contains("<<<<<<<"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
assert!(
|
||||
!content.contains(">>>>>>>"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
assert!(
|
||||
content.contains("feature_fn"),
|
||||
"feature branch addition must be preserved on master"
|
||||
@@ -1346,7 +1362,10 @@ after\n";
|
||||
content.contains("master_fn"),
|
||||
"master branch addition must be preserved on master"
|
||||
);
|
||||
assert!(content.contains("existing"), "original function must be preserved");
|
||||
assert!(
|
||||
content.contains("existing"),
|
||||
"original function must be preserved"
|
||||
);
|
||||
|
||||
// Cleanup: no leftover merge-queue branch or workspace.
|
||||
let branches = Command::new("git")
|
||||
@@ -1444,9 +1463,18 @@ after\n";
|
||||
run_squash_merge(repo, "feature/story-238_gates_fail", "238_gates_fail").unwrap();
|
||||
|
||||
assert!(result.had_conflicts, "conflict must be detected");
|
||||
assert!(result.conflicts_resolved, "additive conflict must be auto-resolved");
|
||||
assert!(!result.gates_passed, "quality gates must fail (script/test exits 1)");
|
||||
assert!(!result.success, "merge must be reported as failed when gates fail");
|
||||
assert!(
|
||||
result.conflicts_resolved,
|
||||
"additive conflict must be auto-resolved"
|
||||
);
|
||||
assert!(
|
||||
!result.gates_passed,
|
||||
"quality gates must fail (script/test exits 1)"
|
||||
);
|
||||
assert!(
|
||||
!result.success,
|
||||
"merge must be reported as failed when gates fail"
|
||||
);
|
||||
assert!(
|
||||
!result.output.is_empty(),
|
||||
"output must contain gate failure details"
|
||||
@@ -1454,7 +1482,10 @@ after\n";
|
||||
|
||||
// Master must NOT have been updated (cherry-pick was blocked by gate failure).
|
||||
let content = fs::read_to_string(repo.join("code.txt")).unwrap();
|
||||
assert!(!content.contains("<<<<<<<"), "master must not contain conflict markers");
|
||||
assert!(
|
||||
!content.contains("<<<<<<<"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
// master_addition was the last commit on master; feature_addition must NOT be there.
|
||||
assert!(
|
||||
!content.contains("feature_addition"),
|
||||
@@ -1508,8 +1539,7 @@ after\n";
|
||||
fs::write(stale_ws.join("leftover.txt"), "stale").unwrap();
|
||||
|
||||
// Run the merge — it should clean up the stale workspace first.
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-stale_test", "stale_test").unwrap();
|
||||
let result = run_squash_merge(repo, "feature/story-stale_test", "stale_test").unwrap();
|
||||
|
||||
assert!(
|
||||
result.success,
|
||||
@@ -1649,8 +1679,7 @@ after\n";
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-216_no_components", "216_no_components")
|
||||
.unwrap();
|
||||
run_squash_merge(repo, "feature/story-216_no_components", "216_no_components").unwrap();
|
||||
|
||||
// No pnpm or frontend references should appear in the output.
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user