The great storkit name conversion

This commit is contained in:
Dave
2026-03-20 12:26:02 +00:00
parent 51d878e117
commit c4e45b2841
25 changed files with 1522 additions and 1333 deletions

View File

@@ -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!(