Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce script to automatically resolve conflicts after treewide changes (such as reformats) #363759

Merged
merged 1 commit into from
Jan 21, 2025
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
6 changes: 6 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -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 <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.
#
Expand Down
16 changes: 16 additions & 0 deletions maintainers/scripts/auto-rebase/README.md
Original file line number Diff line number Diff line change
@@ -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
```
61 changes: 61 additions & 0 deletions maintainers/scripts/auto-rebase/run.sh
Original file line number Diff line number Diff line change
@@ -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)"
46 changes: 46 additions & 0 deletions maintainers/scripts/auto-rebase/test/default.nix
Original file line number Diff line number Diff line change
@@ -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 "[email protected]"
./test/run.sh
'';
installPhase = ''
touch $out
'';
}
11 changes: 11 additions & 0 deletions maintainers/scripts/auto-rebase/test/first.diff
Original file line number Diff line number Diff line change
@@ -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";
}
112 changes: 112 additions & 0 deletions maintainers/scripts/auto-rebase/test/run.sh
Original file line number Diff line number Diff line change
@@ -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 <<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 changes: 11 additions & 0 deletions maintainers/scripts/auto-rebase/test/second.diff
Original file line number Diff line number Diff line change
@@ -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,
}: