21

My question is similar to Git - unchange line endings in already committed file, though the accepted answer seems to be mostly musings, and not helpful in my situation.

I have several fairly large commits. Between my editor and my git configuration at the time, I committed many line ending changes

For example, I changed one line in a file, but my commit changes line endings for the thousands of other lines in the file.

I have not pushed the commits. How do I remove the line ending changes before I push?

I tried

git rebase [last_good_commit]

but it just said

current branch *** is up to date. 
derekbaker783
  • 8,109
  • 4
  • 36
  • 50
Paul Draper
  • 78,542
  • 46
  • 206
  • 285

2 Answers2

19

Simply force a rebase for the commits you haven’t pushed yet, and tell it to apply whitespace fixes to the patches:

git rebase --whitespace=fix -f <last_good_commit> 

Rebase internally works via applying patches, and conveniently git-apply supports fixing the whitespace with the --whitespace option.

mockinterface
  • 14,452
  • 5
  • 28
  • 49
  • 1
    Is it possible to fix just the line endings instead of all whitespace changes? – A Jar of Clay Mar 17 '21 at 12:24
  • The `core.whitespace` controls what is an error, so I think (untested) you can add `-c core.whitespace=blank-at-eol` to limit what is being fixed. This prevents the `space-before-tab` from being fixed, but still fixes *all* trailing whitespace, not just CR at the end of the line. – Matthijs Kooijman May 09 '23 at 13:43
  • Note that this approach seems to fail if subsequent commits make changes to the same file - for the first one the whitespace is fixed, but that seems to cause the second one to fail. I tried adding `--ignore-whitespace`, but that (incorrectly, if I understand the docs correctly) complains about mixing apply and merge options. I have another approach that does work in this case with a bit more manual work, I'll share that in a separate answer. – Matthijs Kooijman May 09 '23 at 14:10
1

One trick I've successfully applied is to use git rebase and dos2unix to fix this automatically.

To to this, run an interactive rebase:

git rebase -i [last_good_commit]

This drops you in an editor listing all the commits to be reapplied. For example:

pick ad31eff3 Some commit message
pick 138fab7d Some other commit message

Now modify each line into exec git checkout xxx . && dos2unix *.txt && git commit -a -C xxx, where both xxx's are the hash of the commit in question, and *.txt lists all the files in which you need to fix line endings.

For the example above, this becomes:

exec git checkout ad31eff3 . && dos2unix *.txt && git commit -a -C ad31eff3
exec git checkout 138fab7d . && dos2unix *.txt && git commit -a -C 138fab7d

Save the file and quit the editor: Git will process each command in turn and you should be left with a modified history where the line endings were never wrong.

Using a regex for generating the commands file

Here's a regex (actually a vim command - adapt it for the regex dialect your editor speaks) that can be used to convert the rebase commands file with pick lines to the correct commands for this fix:

:%s/pick \([^ ]*\) .*/exec git checkout \1 . \&\& dos2unix *.txt \&\& exec git commit -a -C \1

How this this work?

For each commit, this trick runs the following commands:

git checkout xxx .

This updates the current working copy contents to match the original commit (with incorrect line endings) exactly, without touching the branch head (current commit) or index at all.

dos2unix *.txt

This fixes all the line endings in the working copy, replaying dos CRLF with unix LF (you can use unix2dos or whatever other command here if you need some other conversion).

git commit -a -C xxx

This commits all the changes (from the original commit, but with changed line endings), reusing the commit message from the original.

The trick here is that for each commit, the first command switches back to the original files. Normally with a rebase, you just pick commits, but that won't work in this case - once you've fixed the line endings in the first commit, the subsequent commits will likely no longer apply, leaving you to fix the mess. In this case, for e.g. the second commit the git checkout used will just use the files from the original commit (tree) as-is, meaning it completely ignores (and thus also reverts) the line endings fixed for the first commit. But that's ok - the dos2unix command will happily fix them again for every commit, and the end result is a clean history where you cannot tell that the line endings were ever wrong.

A smart reader will notice that we're actually not really using any rebase features, since all commits are created using explicit git commit commands instead of letting rebase handle this. So this just uses rebase to reset to the good commit, generate a list of commits to apply and execute commands in order (and do some useful checking, such as whether the working copy is clean and commands do not fail).

Using this for other things

This approach also works well for other automatic repeatable fixes, such as code formatting. I originally figured out this approach to run astyle on a new codebase without creating a massive fix commit.

Matthijs Kooijman
  • 2,498
  • 23
  • 30