Avoid new commits when the task already contains the integration baseline
Why: repeated task-sync calls changed HEAD and invalidated CI even when main had not advanced. Evidence: SELF red-green regression and 58 task lifecycle tests recorded through make evidence in the task card. Impact: sync still fetches and validates components, creates records only for an actual merge, and reports the complete final source SHA. Refs: U1; policy/merge-queue.md; .task/feature-merge-queue-policy-draft.md
This commit is contained in:
+27
-16
@@ -278,18 +278,26 @@ cmd_sync() {
|
||||
need_repo
|
||||
fetch_target
|
||||
check_worktree before-sync
|
||||
git merge --no-edit "origin/${TARGET_BRANCH}" \
|
||||
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
|
||||
-m "Why: include the current integration baseline before verification." \
|
||||
-m "Evidence: fetched origin/${TARGET_BRANCH}=$(git rev-parse "origin/${TARGET_BRANCH}")." \
|
||||
-m "Impact: integrate upstream changes without rewriting task history." \
|
||||
-m "Refs: task-sync"
|
||||
git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete"
|
||||
local base head card branch slug pc changed=0 ancestry_status
|
||||
base=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||||
if git merge-base --is-ancestor "$base" HEAD; then
|
||||
:
|
||||
else
|
||||
ancestry_status=$?
|
||||
[ "$ancestry_status" -eq 1 ] || die "cannot verify target ancestry"
|
||||
git merge --no-edit "$base" \
|
||||
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
|
||||
-m "Why: include the current integration baseline before verification." \
|
||||
-m "Evidence: fetched origin/${TARGET_BRANCH}=${base}." \
|
||||
-m "Impact: integrate upstream changes without rewriting task history." \
|
||||
-m "Refs: task-sync"
|
||||
changed=1
|
||||
fi
|
||||
git merge-base --is-ancestor "$base" HEAD || die "sync incomplete"
|
||||
check_worktree before-update
|
||||
check_worktree materialize
|
||||
check_worktree after-sync
|
||||
local base head card branch slug pc
|
||||
base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD)
|
||||
head=$(git rev-parse HEAD)
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
slug=$(echo "$branch" | tr '/' '-')
|
||||
card=".task/${slug}.md"
|
||||
@@ -304,18 +312,21 @@ cmd_sync() {
|
||||
done
|
||||
fi
|
||||
if [ -n "$card" ] && [ -f "$card" ]; then
|
||||
printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
|
||||
git add "$card"
|
||||
git commit -q -m "Record the sync checkpoint" \
|
||||
-m "Why: bind the task validation checkpoint to the latest integration baseline." \
|
||||
-m "Evidence: SELF recursive component checks; sync_base=${base}; candidate=${head}." \
|
||||
-m "Impact: task metadata only." -m "Refs: ${card}"
|
||||
if [ "$changed" -eq 1 ]; then
|
||||
printf '| 同步(记录前对象) | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
|
||||
git add "$card"
|
||||
git commit -q -m "Record the sync checkpoint" \
|
||||
-m "Why: bind the task validation checkpoint to the integration baseline." \
|
||||
-m "Evidence: SELF recursive component checks; sync_base=${base}; before_record=${head}." \
|
||||
-m "Impact: task metadata only." -m "Refs: ${card}" -- "$card"
|
||||
fi
|
||||
# JSON receipts carry before/after state; older table rows remain readable.
|
||||
python3 "${SCRIPT_DIR}/check-evidence.py" "$card"
|
||||
else
|
||||
echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2
|
||||
fi
|
||||
echo "[synced] sync_base=$(echo "$base" | cut -c1-10) tested_head=$(echo "$head" | cut -c1-10) checked_at=$(date -u +%FT%TZ)"
|
||||
head=$(git rev-parse HEAD)
|
||||
echo "[synced] sync_base=${base} tested_head=${head} checked_at=$(date -u +%FT%TZ) changed=${changed}"
|
||||
echo "[next] 跑正式验收(make test 等),证据写入任务卡"
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,20 @@ class TaskLifecycleTests(unittest.TestCase):
|
||||
self.git(self.repo, "commit", "-m", "Advance main")
|
||||
self.git(self.repo, "push", "origin", "main")
|
||||
|
||||
def record_task_card(self, wt):
|
||||
branch = self.git(wt, "branch", "--show-current").stdout.strip()
|
||||
r0 = self.git(wt, "rev-parse", "origin/main").stdout.strip()
|
||||
card = wt / ".task" / (branch.replace("/", "-") + ".md")
|
||||
card.parent.mkdir(exist_ok=True)
|
||||
card.write_text(
|
||||
f"# {branch}\n任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n"
|
||||
f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n"
|
||||
f"- 工作区:{wt}(worktree);分支:{branch}\n"
|
||||
)
|
||||
self.git(wt, "add", str(card.relative_to(wt)))
|
||||
self.git(wt, "commit", "-m", "Record fixture task")
|
||||
return card
|
||||
|
||||
def add_submodule(self):
|
||||
child = self.root / "component"
|
||||
self.git(self.root, "init", "--initial-branch=main", str(child))
|
||||
@@ -105,6 +119,76 @@ class TaskLifecycleTests(unittest.TestCase):
|
||||
reading = next(line for line in result.stdout.splitlines() if line.startswith("[required-reading]"))
|
||||
self.assertIn("policy/delivery.md", reading)
|
||||
|
||||
def test_sync_same_base_preserves_head_and_card(self):
|
||||
wt = self.worktree()
|
||||
card = self.record_task_card(wt)
|
||||
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
|
||||
content = card.read_bytes()
|
||||
index = self.git(wt, "ls-files", "--stage").stdout
|
||||
for _ in range(2):
|
||||
result = self.task(wt, "sync")
|
||||
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
|
||||
self.assertEqual(card.read_bytes(), content)
|
||||
self.assertEqual(self.git(wt, "ls-files", "--stage").stdout, index)
|
||||
self.assertEqual(self.git(wt, "status", "--porcelain").stdout, "")
|
||||
self.assertIn("changed=0", result.stdout)
|
||||
self.assertIn(f"tested_head={head}", result.stdout)
|
||||
|
||||
def test_sync_new_base_reports_actual_final_head(self):
|
||||
wt = self.worktree()
|
||||
card = self.record_task_card(wt)
|
||||
self.advance_remote()
|
||||
base = self.git(self.repo, "rev-parse", "HEAD").stdout.strip()
|
||||
result = self.task(wt, "sync")
|
||||
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
|
||||
self.git(wt, "merge-base", "--is-ancestor", base, head)
|
||||
self.assertIn(f"sync_base={base}", result.stdout)
|
||||
self.assertIn(f"tested_head={head}", result.stdout)
|
||||
self.assertIn("changed=1", result.stdout)
|
||||
content = card.read_bytes()
|
||||
self.task(wt, "sync")
|
||||
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
|
||||
self.assertEqual(card.read_bytes(), content)
|
||||
|
||||
def test_sync_preserves_dirty_parent_and_staged_changes(self):
|
||||
for staged in (False, True):
|
||||
with self.subTest(staged=staged):
|
||||
wt = self.worktree(name="feature/staged" if staged else "feature/dirty")
|
||||
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
|
||||
(wt / "tracked").write_text("keep owner work\n")
|
||||
if staged:
|
||||
self.git(wt, "add", "tracked")
|
||||
index = self.git(wt, "ls-files", "--stage").stdout
|
||||
result = self.task(wt, "sync", check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
|
||||
self.assertEqual(self.git(wt, "ls-files", "--stage").stdout, index)
|
||||
self.assertEqual((wt / "tracked").read_text(), "keep owner work\n")
|
||||
self.git(self.repo, "worktree", "remove", "--force", str(wt))
|
||||
|
||||
def test_sync_rejects_unreachable_target_without_changing_head(self):
|
||||
wt = self.worktree()
|
||||
self.record_task_card(wt)
|
||||
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
|
||||
self.git(wt, "remote", "set-url", "origin", str(self.root / "missing.git"))
|
||||
result = self.task(wt, "sync", check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("REMOTE_UNVERIFIED", result.stdout)
|
||||
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
|
||||
|
||||
def test_sync_conflict_is_not_reported_as_synced(self):
|
||||
wt = self.worktree()
|
||||
self.record_task_card(wt)
|
||||
(wt / "tracked").write_text("task choice\n")
|
||||
self.git(wt, "commit", "-am", "Task edit")
|
||||
(self.repo / "tracked").write_text("target choice\n")
|
||||
self.git(self.repo, "commit", "-am", "Target edit")
|
||||
self.git(self.repo, "push", "origin", "main")
|
||||
result = self.task(wt, "sync", check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertNotIn("[synced]", result.stdout)
|
||||
self.assertIn("UU tracked", self.git(wt, "status", "--porcelain").stdout)
|
||||
|
||||
def test_sync_materializes_updated_component(self):
|
||||
child = self.add_submodule()
|
||||
wt = self.worktree()
|
||||
@@ -396,6 +480,7 @@ class TaskLifecycleTests(unittest.TestCase):
|
||||
(wt / ".task/feature-test.md").write_text("# Task\n")
|
||||
self.git(wt, "add", ".task")
|
||||
self.git(wt, "commit", "-m", "Create task card")
|
||||
self.advance_remote()
|
||||
hook = self.repo / ".git/hooks/pre-commit"
|
||||
hook.write_text("#!/bin/sh\nexit 1\n")
|
||||
hook.chmod(0o755)
|
||||
|
||||
Reference in New Issue
Block a user