Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions .github/workflows/autoloop.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,35 @@ Each run executes **one iteration for the single selected program**:
- Program `build-tsb-pandas-typescript-migration` → branch `autoloop/build-tsb-pandas-typescript-migration`

```bash
git fetch origin
git fetch origin main
if git ls-remote --exit-code origin autoloop/{program-name}; then
# Branch exists — check it out and merge the default branch
git checkout -b autoloop/{program-name} origin/autoloop/{program-name}
git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}"
# Branch exists — fetch it too so the ahead/behind counts below are
# computed against up-to-date local copies of the remote tips.
git fetch origin autoloop/{program-name}

ahead=$(git rev-list --count origin/main..origin/autoloop/{program-name})
behind=$(git rev-list --count origin/autoloop/{program-name}..origin/main)

if [ "$ahead" = "0" ] && [ "$behind" != "0" ]; then
# All of the branch's commits are already in main (typical case after a
# successful merge of the previous iteration's PR). A merge here would
# produce a noisy "Merge main into branch" commit that re-exposes every
# historical file as a patch touch — the failure mode that triggers
# gh-aw's E003 (>100 files) when a new PR is opened. Fast-forward the
# canonical branch to main instead. This is lossless because ahead=0
# proves every commit on the branch is already reachable from main.
git checkout -B autoloop/{program-name} origin/main
git push --force-with-lease origin autoloop/{program-name}
elif [ "$ahead" != "0" ] && [ "$behind" != "0" ]; then
# True divergence: branch has unique commits AND main has moved on.
# Merge main into the branch so the iteration builds on the latest code.
git checkout -B autoloop/{program-name} origin/autoloop/{program-name}
git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}"
else
# Already at main (ahead=0, behind=0) or only ahead of main (ahead>0,
# behind=0). Nothing to merge — just check out the branch.
git checkout -B autoloop/{program-name} origin/autoloop/{program-name}
fi
else
# Branch does not exist — create it from the default branch
git checkout -b autoloop/{program-name} origin/main
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sync-branches.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 62 additions & 1 deletion .github/workflows/sync-branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,68 @@ steps:
failed.append(branch)
continue

# Merge the default branch into the program branch
# Determine whether the branch can be fast-forwarded to the default
# branch (ahead=0) or has truly diverged and needs a merge.
ahead_proc = subprocess.run(
["git", "rev-list", "--count", f"origin/{default_branch}..origin/{branch}"],
capture_output=True, text=True,
)
behind_proc = subprocess.run(
["git", "rev-list", "--count", f"origin/{branch}..origin/{default_branch}"],
capture_output=True, text=True,
)
if ahead_proc.returncode != 0 or behind_proc.returncode != 0:
# Don't guess — a failed rev-list with empty stdout would
# otherwise be parsed as 0 below and could trigger an
# incorrect fast-forward that loses commits.
print(f" Failed to compute ahead/behind for {branch}: "
f"ahead.rc={ahead_proc.returncode} stderr={ahead_proc.stderr.strip()!r} "
f"behind.rc={behind_proc.returncode} stderr={behind_proc.stderr.strip()!r}")
failed.append(branch)
continue
try:
ahead = int(ahead_proc.stdout.strip())
behind = int(behind_proc.stdout.strip())
except ValueError:
print(f" Failed to parse ahead/behind counts for {branch}: "
f"ahead={ahead_proc.stdout!r} behind={behind_proc.stdout!r}")
failed.append(branch)
continue

if behind == 0:
# Branch already contains every commit on the default branch.
print(f" {branch} is already up to date with {default_branch} (ahead={ahead}, behind=0)")
continue

if ahead == 0:
# Lossless fast-forward: every commit on the branch is already
# reachable from the default branch (typical case once the
# previous iteration's PR has been merged). A real merge here
# would create a "Merge default into branch" commit that re-
# exposes every historical file as a patch touch — the noise
# that trips gh-aw's MAX_FILES limit when the next iteration
# opens a new PR. Reset the branch to the default branch's HEAD
# and force-push (with lease) instead.
reset = subprocess.run(
["git", "reset", "--hard", f"origin/{default_branch}"],
capture_output=True, text=True,
)
if reset.returncode != 0:
print(f" Failed to fast-forward {branch}: {reset.stderr}")
failed.append(branch)
continue
push = subprocess.run(
["git", "push", "--force-with-lease", "origin", branch],
capture_output=True, text=True,
)
if push.returncode != 0:
print(f" Failed to push fast-forward of {branch}: {push.stderr}")
failed.append(branch)
continue
print(f" Fast-forwarded {branch} to {default_branch} (was behind by {behind})")
continue

# True divergence (ahead>0 and behind>0): merge the default branch in.
merge = subprocess.run(
["git", "merge", f"origin/{default_branch}", "--no-edit",
"-m", f"Merge {default_branch} into {branch}"],
Expand Down
Loading