Reputation: 983
We introduced php-cs-fixer
into our codebase and I want to rebase an existing feature branch. We enforce a semi-linear history, so every feature branch is rebased before merging without squashing the whole branch.
To keep the history clean I want to run the tool on every commit in the branch, but keep the commits in place and not have the code style change with a later commit, but already be correct in every commit that will later be merged into the main
branch.
Using git rebase --exec "./vendor/bin/php-cs-fixer fix" main
results in a lot of conflicts that I don't want to fix manually, as it is very error-prone.
Upvotes: 0
Views: 533
Reputation: 983
I think I came up with a better solution to the problem that doesn't require any additional dependencies and works as part of a normal git rebase
and doesn't cause any conflicts.
With these commands we can simulate a cherry-pick
and inject our code style fixer before the commit:
H="..." #Commit hash
F=$(git show --pretty="" --name-only $H ) # Files added/changed in commit
git checkout $H -- $F # Get changes from commit into working directory
./vendor/bin/php-cs-fixer fix $F # Run code-style-fixer replace with your own
git add $F # Add the files to commit
git commit -C $H # New commit, but copy message and authorship info from original commit
git restore . # Optional: In case your code-style-fixer changed any other files. Revert those changes
So we start an interactive rebase with git rebase -i
and then replace all pick
operations with exec
operations for those commands.
For simplicity’s sake I assume sh fix_cs.sh
runs the code style fixer.
vim
as your GIT_EDITOR
:Press : and then enter or middle-click-paste this replacement command: %s@^pick \(\w\+\).*@exec H="\1"; F=$(git show --pretty="" --name-only $H); git checkout $H -- $F \&\& sh fix_cs.sh $F \&\& git add $F \&\& git commit -C $H \&\& git restore .@
nano
as your GIT_EDITOR
:^pick (\w+).*
exec H="\1"; F=$(git show --pretty="" --name-only $H); git checkout $H -- $F && sh fix_cs.sh $F && git add $F && git commit -C $H && git restore .
sed
to do the replacements with custom GIT_EDITOR
:This will do the replacements with sed
, then launch your $EDITOR
or vim
for you to review the changes.
GIT_EDITOR='function a { sed -i -E '"'"'s@^pick (\w+).*@exec H="\1"; F=$(git show --pretty="" --name-only $H); git checkout $H -- $F \&\& sh fix_cs.sh $F \&\& git add $F \&\& git commit -C $H \&\& git restore . @'"'"' $1;'"${GIT_EDITOR:-${EDITOR:-vim}}"' $1;};a' \
git rebase -i HEAD^
Use GIT_EDITOR=vim git rebase -i ...
to do the rebase or try if the nano
replacements also work in your editor.
If you do a --root
rebase this method will create a new empty root commit, but you can get rid of that easily with a second rebase.
Upvotes: 0
Reputation: 41
I just had the exact same issue and this is one of the first hits on google. I solved it using git-filter-repo and its contrib tool lint-history:
This is a simple program that will run a linting program on all non-binary files in history.
[...]
To run eslint --fix on all .js files in history:
lint-history --relevant 'return filename.endswith(b".js")' eslint --fix
Do note, that those tools rewrite your entire git history so do a backup first. Also, if you already published your changes, you better coordinate the change with your team. I had some issues running it on Windows, but using WSL worked fine.
Upvotes: 1
Reputation: 983
I came up with this semi-automatic approach:
I started with git rebase -i main
and replaced every pick
operation with an edit
operation. (Vim: :%s/pick/edit/
)
Instead of conflict resolving manually, I use git checkout REBASE_HEAD .
, to replace the working tree with the non-code formatted version and then run the code formatting tool again. (In this example ./vendor/bin/php-cs-fixer fix
)
Because the rebase behavior is slightly different if a conflict occurs, you need to follow up with different commands to complete the current commit based on the state:
Looks like
Stopped at abc123... [Commit Message...]
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
After checkout and code format, amend current commit and continue rebase:
git commit --amend -a --no-edit && git rebase --continue
Complete one-step command:
git checkout REBASE_HEAD . && ./vendor/bin/php-cs-fixer fix && git commit --amend -a --no-edit && git rebase --continue
Looks like this (hints might be colored):
Auto-merging [File]
CONFLICT (content): Merge conflict in [File]
error: could not apply abc123... [Commit Message]
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply abc123... [Commit Message]
After checkout and code format, stage changes and let git rebase --continue
amend the commit:
git add -u . && GIT_EDITOR=true git rebase --continue
Complete one-step command:
git checkout REBASE_HEAD . && ./vendor/bin/php-cs-fixer fix && git add -u . && GIT_EDITOR=true git rebase --continue
If you use the wrong command, the end result will be the same, but you will lose some commits.
Sadly, I couldn't figure out a way to use git rebase --exec
(REBASE_HEAD
isn't defined during the exec command?) or a way to automatically use the correct resolve command.
I'm sure there is a better solution, but I couldn't find it, so I present mine here.
Upvotes: 0