diff --git a/.task/feature-proposal-status-20261012.md b/.task/archive/feature-proposal-status-20261012.md similarity index 100% rename from .task/feature-proposal-status-20261012.md rename to .task/archive/feature-proposal-status-20261012.md diff --git a/.task/feature-rdm-audit-export-entry.md b/.task/archive/feature-rdm-audit-export-entry.md similarity index 100% rename from .task/feature-rdm-audit-export-entry.md rename to .task/archive/feature-rdm-audit-export-entry.md diff --git a/.task/feature-u7-account-tx-audit.md b/.task/archive/feature-u7-account-tx-audit.md similarity index 100% rename from .task/feature-u7-account-tx-audit.md rename to .task/archive/feature-u7-account-tx-audit.md diff --git a/scripts/task.sh b/scripts/task.sh index 902c8b7e..41212aa3 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -57,7 +57,9 @@ cmd_precheck() { pb=$(card_of_branch "$pc"); [ -n "$pb" ] || continue pst=$(card_status "$pc"); case "$pst" in 已完成*) continue ;; esac card_pending "${pc##*/}" && continue - if git log --merges --format='%s' "origin/${TARGET_BRANCH}" 2>/dev/null | grep -qF " from $pb into ${TARGET_BRANCH}"; then + # Consume the complete producer output: grep -q exits on the first match, + # making git log die with SIGPIPE (141 under pipefail) on long histories. + if git log --merges --format='%s' "origin/${TARGET_BRANCH}" 2>/dev/null | grep -F " from $pb into ${TARGET_BRANCH}" >/dev/null; then open_cards="${open_cards} ${pc##*/}" fi done @@ -379,7 +381,7 @@ card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\ card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; } card_merge() { # $1=branch → "sha|subject"(该分支合入 origin/ 的合并提交) git log --merges --format='%H|%s' "origin/${TARGET_BRANCH}" 2>/dev/null \ - | grep -F " from $1 into ${TARGET_BRANCH}" | head -1 + | grep -F " from $1 into ${TARGET_BRANCH}" | sed -n '1p' } card_pending() { # $1=卡片文件名——闭合是否已在某 origin/feature/card-close-* 分支待合入 local cb diff --git a/tests/test_task.py b/tests/test_task.py index 323797b6..e71f5faa 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -758,6 +758,35 @@ class TaskLifecycleTests(unittest.TestCase): self.git(publisher, "push", "origin", "main") return self.git(publisher, "rev-parse", "HEAD").stdout.strip(), card.read_text() + def test_long_merge_history_keeps_merged_card_visible_and_closable(self): + # A recent match makes grep -q/head exit before git log has finished + # writing a large history. With pipefail, its SIGPIPE used to hide the + # card from precheck and leave task-close with nothing to archive. + base = self.git(self.repo, "rev-parse", "HEAD").stdout.strip() + tree = self.git(self.repo, "rev-parse", "HEAD^{tree}").stdout.strip() + side = self.git(self.repo, "commit-tree", tree, "-p", base, + "-m", "Fixture side commit").stdout.strip() + tip = base + for i in range(220): + tip = self.git(self.repo, "commit-tree", tree, "-p", tip, "-p", side, + "-m", f"Older fixture merge {i} " + "x" * 512).stdout.strip() + self.git(self.repo, "update-ref", "refs/heads/main", tip) + self.git(self.repo, "push", "origin", "main") + actual_merge, _ = self.publish_merged_task() + self.git(self.repo, "fetch", "origin") + self.git(self.repo, "merge", "--ff-only", "origin/main") + + precheck = self.task(self.repo, "precheck") + self.assertIn("[cards] 已合入未闭合任务卡:feature-merged.md", precheck.stdout) + closed = self.task(self.repo, "close") + self.assertIn("[closed] 1 张已合入卡片", closed.stdout) + refs = self.git(self.repo, "for-each-ref", "--format=%(refname:short)", + "refs/heads/feature/card-close-*").stdout.splitlines() + self.assertEqual(len(refs), 1) + archived = self.git(self.repo, "show", f"{refs[0]}:.task/archive/feature-merged.md").stdout + self.assertIn("状态:已完成(回填)", archived) + self.assertIn(actual_merge, archived) + def assert_post_merge_only_reminds_owner(self, operation): expected_head, expected_card = self.publish_merged_task() self.git(self.repo, "fetch", "origin")