diff --git a/CHANGELOG.md b/CHANGELOG.md index 90610806..012c20a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ per-file diff commands. ### Fixed +- **Upstream triage emits cherry-pick suggestions in dependency order** + (`tools/upstream_triage.py`) - ready-to-run commands now list ancestors before + descendants, so a follow-up patch is not suggested before the change it edits. + The report tables remain newest first; the tool still executes no Git mutations. + - **`/upskill` and `/notion-sync` now name the field ranked entries actually carry when joining them to tracker rows** (`.claude/skills/upskill/SKILL.md` Step 3, `.claude/commands/notion-sync.md` Step 2, `tests/test_upskill_skill.py`, `tests/test_notion_sync_command.py`) - both said to match diff --git a/tests/test_upstream_triage.py b/tests/test_upstream_triage.py index 73c7814c..de91a40a 100644 --- a/tests/test_upstream_triage.py +++ b/tests/test_upstream_triage.py @@ -137,6 +137,29 @@ class AlreadyAppliedTests(TriageRepoFixture): self.assertIn("**1** worth reviewing", result.stdout) +class SuggestedCherryPickOrderTests(TriageRepoFixture): + def test_suggested_commands_apply_dependent_changes_in_ancestry_order(self): + self.write("kept.py", "print('first upstream change')\n") + first = self.commit("Introduce shared behavior") + self.write("kept.py", "print('second upstream change')\n") + second = self.commit("Refine shared behavior") + self.set_upstream_to_head() + git(self.root, "reset", "--hard", "HEAD~2") + self.write("fork_only.txt", "fork customization\n") + self.commit("Customize fork") + + result = self.run_triage() + self.assertEqual(result.returncode, 0, result.stderr) + commands = [line.split(" #", 1)[0].split() for line in result.stdout.splitlines() + if line.startswith("git cherry-pick ")] + self.assertEqual(len(commands), 2, result.stdout) + for command in commands: + replay = subprocess.run(command, cwd=self.root, capture_output=True, text=True) + self.assertEqual(replay.returncode, 0, replay.stdout + replay.stderr) + self.assertEqual([command[2] for command in commands], [first, second]) + self.assertEqual((self.root / "kept.py").read_text(), "print('second upstream change')\n") + + class WontPortTests(TriageRepoFixture): def test_listed_sha_is_excluded(self): self.write("kept.py", "print('rejected feature')\n") diff --git a/tools/upstream_triage.py b/tools/upstream_triage.py index bd73e7ce..9f6a83a2 100755 --- a/tools/upstream_triage.py +++ b/tools/upstream_triage.py @@ -56,7 +56,7 @@ def git(*args: str) -> str: def rev_list(range_spec: str) -> list[str]: - out = git("rev-list", "--no-merges", range_spec).strip() + out = git("rev-list", "--no-merges", "--topo-order", range_spec).strip() return out.splitlines() if out else [] @@ -203,7 +203,8 @@ def main() -> int: lines.append("
Ready-to-run cherry-picks (review each before running)") lines.append("") lines.append("```bash") - for short, sha, subj, _ in review: + # Report rows stay newest first; commands must apply ancestors first. + for short, sha, subj, _ in reversed(review): lines.append(f"git cherry-pick {sha} # {subj}") lines.append("```") lines.append("")