Donate to support Ukraine's independence.

11 Aug'23

Rebasing all relevant pull requests on a Github repo

If you chose rebasing as your primary git workflow strategy, it becomes important to keep your feature branches updated. Out of the box, Github offers automatic rebasing on many Dependabot pull requests. Dependabot PRs can be further rebased semi-automatically using the @dependabot rebase command issued in the comments.

For all other PRs, you need to set up a bot or a Github Action to do this automatically. One such action that could be used is peter-evans/rebase:

name: Rebase
on:
  schedule:
    - cron:  '40 7 * * *'
  workflow_dispatch:

jobs:
  rebase:
    runs-on: ubuntu-latest
    steps:
      - uses: peter-evans/rebase@v2
        with:
          # token: ${{ secrets.GH_PAT }} 
          # exclude-drafts: true
          exclude-labels: |
            no-rebase
            dependencies

The action works reliably and allows to skip all Dependabot PRs (tagged with a dependencies label). The latter is important to do because the Dependabot refuses to automatically update its PR once you make any manual changes to it, like rebasing it yourself. The token parameter allows the action to push on your behalf. Otherwise, Github Actions pushes do not trigger other Github Actions (mainly, CI). This means that you will only be able to know if the code has any conflicts with the main branch, but not whether it is still safe to merge.

If this approach is not viable for you (e.g., you don’t have admin rights on the repo or don’t trust the action with your PAT token), you need to rebase on your machine. Below I provide a script (licensed under the Simplified BSD license) that rebases all PRs except those made by Dependabot:

#!/usr/bin/env bash

# Copyright 2023 Andrew Berezovskyi
# SPDX-License-Identifier: BSD-2-Clause

set -uo pipefail
set -e
# set -x

err=0

git_main_branch () {
    command git rev-parse --git-dir &> /dev/null || return
    local ref
    for ref in refs/{heads,remotes/{origin,upstream}}/{main,trunk,mainline,default}
    do
        if command git show-ref -q --verify $ref
        then
            echo ${ref:t}
            return
        fi
    done
    echo master
}

if ! eval git diff --quiet ; then
    >&2 echo "Git repo is not clean. Commit all the changes first."
    exit 1
fi

CURR_BR="$(git branch --show-current)"
MAIN_BR="$(git_main_branch)"

trap 'git checkout "${CURR_BR}"' err exit SIGINT SIGTERM

git checkout "${MAIN_BR}"
git pull --ff-only

PR_LIST="$(sort <(gh pr list) <(gh pr list -A app/dependabot) | uniq -u | cut -f1)"


while IFS="" read -r pr || [ -n "$pr" ]
do
  printf 'Processing PR #%s\n' "$pr"
  gh pr checkout "$pr" || err=$?
  # --reapply-cherry-picks if needed
  # https://stackoverflow.com/questions/61905448/git-cherry-pick-and-then-rebase
  [ "$err" -eq "128" ] && git rebase 
  # rebase -i if needed
  git rebase "${MAIN_BR}" && git pull --rebase origin "${MAIN_BR}" && git push --force-with-lease
done <<< "${PR_LIST}"

Some explanation for the script:

  • git pull --ff-only is done under the assumption that you don’t commit to the main branch yourself and thus, no conflicts should ever exist on that branch.
  • git_main_branch() allows the script to be used with the repos that use main, master, or trunk main branch.
  • sort X Y | uniq -u pattern is used because Github has a bad support for negative patterns as of 2023.
  • git push --force-with-lease prevents you from overwriting any commits during a force-push that were made by someone else, thus providing some degree of safety.
  • [ "$err" -eq "128" ] && git rebase is done to rebase your local feature branch first, in case someone else made a similar rebase before you.
  • trap "git checkout '${CURR_BR}'" will restore the checked out branch you were on before running the script (whether the scripts succeeds or fails).
  • set -u makes the script fail if any undefined variables are used, set -o pipefail makes a piped command fail if any of its stages fail (e.g. if gzip fails in tar | gzip | gpg), set -e terminates the script immediately after the first error, and set -x could be used to print every command that this script runs.

You can make the script available anywhere in your system if you put the script under /usr/local/bin/ (or $HOME/.local/bin just for your user account) and make it executable: chmod +x /usr/local/bin/git-rebase-all.

Finally, this script only requires the Github CLI and plain old Bash to be installed on your system for this to work. Happy rebasing! 🦸‍♂️

Category: 

Comments