From b0cfc9769a0845c7506351fa7147a82495cfc95f Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Tue, 10 Dec 2024 00:45:55 +0100 Subject: [PATCH] maintainers/scripts/auto-rebase: init --- .git-blame-ignore-revs | 6 + maintainers/scripts/auto-rebase/README.md | 16 +++ maintainers/scripts/auto-rebase/run.sh | 61 ++++++++++ .../scripts/auto-rebase/test/default.nix | 46 +++++++ .../scripts/auto-rebase/test/first.diff | 11 ++ maintainers/scripts/auto-rebase/test/run.sh | 112 ++++++++++++++++++ .../scripts/auto-rebase/test/second.diff | 11 ++ 7 files changed, 263 insertions(+) create mode 100644 maintainers/scripts/auto-rebase/README.md create mode 100755 maintainers/scripts/auto-rebase/run.sh create mode 100644 maintainers/scripts/auto-rebase/test/default.nix create mode 100644 maintainers/scripts/auto-rebase/test/first.diff create mode 100755 maintainers/scripts/auto-rebase/test/run.sh create mode 100644 maintainers/scripts/auto-rebase/test/second.diff diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c3ca7bf191124..2892d07f97d90 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,5 +1,11 @@ # This file contains a list of commits that are not likely what you # are looking for in a blame, such as mass reformatting or renaming. +# +# If a commit's line ends with `# !autorebase `, +# where is an idempotent bash command that reapplies the changes from the commit, +# the `maintainers/scripts/auto-rebase/run.sh` script can be used to rebase +# across that commit while automatically resolving merge conflicts caused by the commit. +# # You can set this file as a default ignore file for blame by running # the following command. # diff --git a/maintainers/scripts/auto-rebase/README.md b/maintainers/scripts/auto-rebase/README.md new file mode 100644 index 0000000000000..926aa6c99d9fb --- /dev/null +++ b/maintainers/scripts/auto-rebase/README.md @@ -0,0 +1,16 @@ +# Auto rebase script + +The [`./run.sh` script](./run.sh) in this directory rebases the current branch onto a target branch, +while automatically resolving merge conflicts caused by marked commits in [`.git-blame-ignore-revs`](../../../.git-blame-ignore-revs). +See the header comment of that file to understand how to mark commits. + +This is convenient for resolving merge conflicts for pull requests after e.g. treewide reformats. + +## Testing + +To run the tests in the [test directory](./test): +``` +$ cd test +$ nix-shell +nix-shell> ./run.sh +``` diff --git a/maintainers/scripts/auto-rebase/run.sh b/maintainers/scripts/auto-rebase/run.sh new file mode 100755 index 0000000000000..1a8ad98c37235 --- /dev/null +++ b/maintainers/scripts/auto-rebase/run.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +if (( $# < 1 )); then + echo "Usage: $0 TARGET_BRANCH" + echo "" + echo "TARGET_BRANCH: Branch to rebase the current branch onto, e.g. master or release-24.11" + exit 1 +fi + +targetBranch=$1 + +# Loop through all autorebase-able commits in .git-blame-ignore-revs on the base branch +readarray -t autoLines < <( + git show "$targetBranch":.git-blame-ignore-revs \ + | sed -n 's/^\([0-9a-f]\+\).*!autorebase \(.*\)$/\1 \2/p' +) +for line in "${autoLines[@]}"; do + read -r autoCommit autoCmd <<< "$line" + + if ! git cat-file -e "$autoCommit"; then + echo "Not a valid commit: $autoCommit" + exit 1 + elif git merge-base --is-ancestor "$autoCommit" HEAD; then + # Skip commits that we have already + continue + fi + + echo -e "\e[32mAuto-rebasing commit $autoCommit with command '$autoCmd'\e[0m" + + # The commit before the commit + parent=$(git rev-parse "$autoCommit"~) + + echo "Rebasing on top of the previous commit, might need to manually resolve conflicts" + if ! git rebase --onto "$parent" "$(git merge-base "$targetBranch" HEAD)"; then + echo -e "\e[33m\e[1mRestart this script after resolving the merge conflict as described above\e[0m" + exit 1 + fi + + echo "Reapplying the commit on each commit of our branch" + # This does two things: + # - The parent filter inserts the auto commit between its parent and + # and our first commit. By itself, this causes our first commit to + # effectively "undo" the auto commit, since the tree of our first + # commit is unchanged. This is why the following is also necessary: + # - The tree filter runs the command on each of our own commits, + # effectively reapplying it. + FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch \ + --parent-filter "sed 's/$parent/$autoCommit/'" \ + --tree-filter "$autoCmd" \ + "$autoCommit"..HEAD + + # A tempting alternative is something along the lines of + # git rebase --strategy-option=theirs --onto "$rev" "$parent" \ + # --exec '$autoCmd && git commit --all --amend --no-edit' \ + # but this causes problems because merges are not guaranteed to maintain the formatting. + # The ./test.sh exercises such a case. +done + +echo "Rebasing on top of the latest target branch commit" +git rebase --onto "$targetBranch" "$(git merge-base "$targetBranch" HEAD)" diff --git a/maintainers/scripts/auto-rebase/test/default.nix b/maintainers/scripts/auto-rebase/test/default.nix new file mode 100644 index 0000000000000..fd6ce30d24ac3 --- /dev/null +++ b/maintainers/scripts/auto-rebase/test/default.nix @@ -0,0 +1,46 @@ +let + pkgs = import ../../../.. { + config = { }; + overlays = [ ]; + }; + + inherit (pkgs) + lib + stdenvNoCC + gitMinimal + treefmt + nixfmt-rfc-style + ; +in + +stdenvNoCC.mkDerivation { + name = "test"; + src = lib.fileset.toSource { + root = ./..; + fileset = lib.fileset.unions [ + ../run.sh + ./run.sh + ./first.diff + ./second.diff + ]; + }; + nativeBuildInputs = [ + gitMinimal + treefmt + nixfmt-rfc-style + ]; + patchPhase = '' + patchShebangs . + ''; + + buildPhase = '' + export HOME=$(mktemp -d) + export PAGER=true + git config --global user.email "Your Name" + git config --global user.name "your.name@example.com" + ./test/run.sh + ''; + installPhase = '' + touch $out + ''; +} diff --git a/maintainers/scripts/auto-rebase/test/first.diff b/maintainers/scripts/auto-rebase/test/first.diff new file mode 100644 index 0000000000000..0485d72102a7b --- /dev/null +++ b/maintainers/scripts/auto-rebase/test/first.diff @@ -0,0 +1,11 @@ +diff --git a/b.nix b/b.nix +index 9d18f25..67b0466 100644 +--- a/b.nix ++++ b/b.nix +@@ -1,5 +1,5 @@ + { + this = "is"; + +- some = "set"; ++ some = "value"; + } diff --git a/maintainers/scripts/auto-rebase/test/run.sh b/maintainers/scripts/auto-rebase/test/run.sh new file mode 100755 index 0000000000000..04989ba9a30ce --- /dev/null +++ b/maintainers/scripts/auto-rebase/test/run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# https://stackoverflow.com/a/246128/6605742 +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Allows using a local directory for temporary files, +# which can then be inspected after the run +if (( $# > 0 )); then + tmp=$(realpath "$1/tmp") + if [[ -e "$tmp" ]]; then + rm -rf "$tmp" + fi + mkdir -p "$tmp" +else + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' exit +fi + +# Tests a scenario where two poorly formatted files were modified on both the +# main branch and the feature branch, while the main branch also did a treewide +# format. + +git init "$tmp/repo" +cd "$tmp/repo" || exit +git branch -m main + +# Some initial poorly-formatted files +cat > a.nix < b.nix < treefmt.toml < .git-blame-ignore-revs +git add -A +git commit -a -m "update ignored revs" + +git switch feature + +# Setup complete + +git log --graph --oneline feature main + +# This expectedly fails with a merge conflict that has to be manually resolved +"$SCRIPT_DIR"/../run.sh main && exit 1 +sed '/<<>>/d' -i a.nix +git add a.nix +GIT_EDITOR=true git rebase --continue + +"$SCRIPT_DIR"/../run.sh main + +git log --graph --oneline feature main + +checkDiff() { + local ref=$1 + local file=$2 + expectedDiff=$(cat "$file") + actualDiff=$(git diff "$ref"~ "$ref") + if [[ "$expectedDiff" != "$actualDiff" ]]; then + echo -e "Expected this diff:\n$expectedDiff" + echo -e "But got this diff:\n$actualDiff" + exit 1 + fi +} + +checkDiff HEAD~ "$SCRIPT_DIR"/first.diff +checkDiff HEAD "$SCRIPT_DIR"/second.diff + +echo "Success!" diff --git a/maintainers/scripts/auto-rebase/test/second.diff b/maintainers/scripts/auto-rebase/test/second.diff new file mode 100644 index 0000000000000..3b714658aa23c --- /dev/null +++ b/maintainers/scripts/auto-rebase/test/second.diff @@ -0,0 +1,11 @@ +diff --git a/a.nix b/a.nix +index 18ba7ce..bcf38bc 100644 +--- a/a.nix ++++ b/a.nix +@@ -1,6 +1,5 @@ + { + x, +- why, + + z, + }: