maintainers/scripts/auto-rebase: init

Changed files
+263
maintainers
+6
.git-blame-ignore-revs
···
# 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.
# You can set this file as a default ignore file for blame by running
# the following command.
#
···
# 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 <command>`,
+
# where <command> 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.
#
+16
maintainers/scripts/auto-rebase/README.md
···
···
+
# 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
+
```
+61
maintainers/scripts/auto-rebase/run.sh
···
···
+
#!/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)"
+46
maintainers/scripts/auto-rebase/test/default.nix
···
···
+
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
+
'';
+
}
+11
maintainers/scripts/auto-rebase/test/first.diff
···
···
+
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";
+
}
+112
maintainers/scripts/auto-rebase/test/run.sh
···
···
+
#!/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 <<EOF
+
{ x
+
, y
+
+
, z
+
}:
+
null
+
EOF
+
+
cat > b.nix <<EOF
+
{
+
this = "is";
+
+
+
some="set" ;
+
}
+
EOF
+
+
git add -A
+
git commit -m "init"
+
+
git switch -c feature
+
+
# Some changes
+
sed 's/set/value/' -i b.nix
+
git commit -a -m "change b"
+
sed '/, y/d' -i a.nix
+
git commit -a -m "change a"
+
+
git switch main
+
+
# A change to cause a merge conflict
+
sed 's/y/why/' -i a.nix
+
git commit -a -m "change a"
+
+
cat > treefmt.toml <<EOF
+
[formatter.nix]
+
command = "nixfmt"
+
includes = [ "*.nix" ]
+
EOF
+
git add -A
+
git commit -a -m "introduce treefmt"
+
+
# Treewide reformat
+
treefmt
+
git commit -a -m "format"
+
+
echo "$(git rev-parse HEAD) # !autorebase treefmt" > .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!"
+11
maintainers/scripts/auto-rebase/test/second.diff
···
···
+
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,
+
}: