diff --git a/tools/release/sync-release.sh b/tools/release/sync-release.sh index 05a629e7e..26f23d9ce 100755 --- a/tools/release/sync-release.sh +++ b/tools/release/sync-release.sh @@ -78,14 +78,28 @@ fi old_local="" old_local="$(git rev-parse -q --verify "refs/heads/$TARGET^{commit}" 2>/dev/null)" || old_local="" -# No-op only if the existing target has exactly this tree AND this parent. +# main may hold commits develop lacks (earlier release merges, hotfixes). Then +# release also takes main as a second parent: the tree stays the filtered source +# tree, and main can always fast-forward to release -- the PR never conflicts. +main_commit="" +for ref in refs/remotes/origin/main refs/heads/main; do + main_commit="$(git rev-parse -q --verify "$ref^{commit}" 2>/dev/null)" && break + main_commit="" +done +parents="$src_commit" +if [ -n "$main_commit" ] && ! git merge-base --is-ancestor "$main_commit" "$src_commit"; then + parents="$src_commit $main_commit" +fi + +# No-op only if the existing target has exactly this tree AND these parents. if [ -n "$old_local" ] \ && [ "$(git rev-parse "$old_local^{tree}")" = "$new_tree" ] \ - && [ "$(git rev-parse -q --verify "$old_local^" 2>/dev/null || true)" = "$src_commit" ]; then + && [ "$(git rev-list --parents -n 1 "$old_local" | cut -d' ' -f2-)" = "$parents" ]; then echo "no changes: $TARGET already regenerated from $short" new_commit="$old_local" else - new_commit="$(git commit-tree "$new_tree" -p "$src_commit" -m "Release: sync from $SOURCE $short")" + pargs=""; for p in $parents; do pargs="$pargs -p $p"; done + new_commit="$(git commit-tree "$new_tree" $pargs -m "Release: sync from $SOURCE $short")" git update-ref "refs/heads/$TARGET" "$new_commit" echo "$TARGET -> $(git rev-parse --short "$new_commit") (parent $short)" fi diff --git a/tools/release/test-sync-release.sh b/tools/release/test-sync-release.sh index e5cbe7080..cba1d3139 100755 --- a/tools/release/test-sync-release.sh +++ b/tools/release/test-sync-release.sh @@ -130,6 +130,31 @@ git update-ref refs/remotes/origin/release "$stale" bash "$SYNC" --push >"$SCRATCH/push3.log" 2>&1 check "push overwrites stale remote release" test "$(git --git-dir="$SCRATCH/remote.git" rev-parse refs/heads/release)" = "$(git rev-parse release)" +# main has diverged from develop (a commit main has and develop lacks, with a +# conflicting edit): release must still fast-forward main, keep develop's tree. +git branch -D release -q 2>/dev/null +base="$(git rev-parse HEAD~1)" +git checkout -q -b main "$base" +echo "main-only edit" > LICENSE; git add LICENSE; git commit -q -m "main-only" +main_tip="$(git rev-parse HEAD)" +git checkout -q develop +dev_tip="$(git rev-parse HEAD)" +bash "$SYNC" >"$SCRATCH/main1.log" 2>&1 +check "diverged main: first parent is source" test "$(git rev-parse release^1)" = "$dev_tip" +check "diverged main: second parent is main" test "$(git rev-parse -q --verify release^2)" = "$main_tip" +check "diverged main: main fast-forwards to release" git merge-base --is-ancestor "$main_tip" release +check "diverged main: tree is develop's, not main's" test "$(git show release:LICENSE)" = "$(git show develop:LICENSE)" +bash "$SYNC" >"$SCRATCH/main2.log" 2>&1 +h_main="$(git rev-parse release)" +bash "$SYNC" >"$SCRATCH/main3.log" 2>&1 +check "diverged main: repeat run is a no-op" test "$(git rev-parse release)" = "$h_main" +# once main is an ancestor of develop, no second parent is added +git branch -f main "$base" +git branch -D release -q +bash "$SYNC" >"$SCRATCH/main4.log" 2>&1 +check "main behind develop: single parent" bash -c '! git rev-parse -q --verify release^2 >/dev/null' +git branch -D main -q + # Refuses when target is checked out git checkout -q release 2>/dev/null bash "$SYNC" >/dev/null 2>&1; rc=$?