MaPePeR
MaPePeR

Reputation: 983

How to run code formatting tool on every commit in feature-branch with git rebase

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

Answers (3)

MaPePeR
MaPePeR

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.

If you use 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 .@

if you use nano as your GIT_EDITOR:
  • Go into Search-Replace mode by Pressing Ctrl+\
  • Make sure you are in Regexp replace mode. Press Alt+R to switch
  • Search [Regexp] to replace: ^pick (\w+).*
  • Replace with: 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 .
  • Press A to replace all occurrences.
Using 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^
Otherwise

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

okolaris
okolaris

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

MaPePeR
MaPePeR

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:

This command if you encounter a normal "edit" breakpoint:

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

This command if you encounter a rebase conflict:

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

Related Questions