1
0
Fork 1
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-03-06 23:31:34 +00:00

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

This commit is contained in:
Janne Heß 2025-01-21 20:47:57 +01:00 committed by GitHub
commit 3425323bb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 263 additions and 0 deletions

View file

@ -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.
#

View file

@ -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
```

View file

@ -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)"

View file

@ -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
'';
}

View file

@ -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";
}

View file

@ -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!"

View file

@ -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,
}: