27

Suppose you have:

A-B-C

Now your build/test fails. The fix should be merged in A. My current work-flow is like this:

$ git commit -m "fixA"

A-B-C-fixA

$ git rebase -i A~1

And squash fixA in A, result in:

A'-B-C

Is there a command to do something like:

A-B-C + (index with fix for A)

$ git commit -supperdupper A 

Result:

A'-B-C
5
  • 1
    Why such a weird requirement that fixA has to be merged in A?
    – Marcin Gil
    Commented Oct 15, 2008 at 12:26
  • 12
    because it should have been in A in the first place.
    – elmarco
    Commented Oct 15, 2008 at 14:27
  • 4
    I do the same thing; I'm not sure why people think it's weird. If you are trying to organize commits into small, logically-grouped pieces, then it's natural to be preparing several unreleased commits at once. (You might not know if you are really done with A until you finish C).
    – andy
    Commented Oct 20, 2008 at 6:32
  • I always do the same thing, too - and I've been longing for something like this (I like to call it git fixup <rev>) for quite some time. Commented Jul 5, 2010 at 14:55
  • similar: stackoverflow.com/questions/3103589/…
    – anarcat
    Commented Dec 8, 2015 at 21:47

5 Answers 5

21

If you're just looking for the easy solution for fixing up earlier commits, read the question! It explains it all. But since Elmarco was asking for a slick way, here we go:

As of Git 1.7.0, there is an --autosquash option for rebase, which does what you want. There is also the --fixup and --squash options for commit to make things easier. With some aliasing you can probably even get the whole thing into a single command.

I'd suggest upgrading to the newest Git for maximum awesomeness:

git/Documentation/RelNotes $ grep -i -A1 autosquash\\\|fixup *
1.7.0.txt: * "git rebase -i" learned new action "fixup" that squashes the change
1.7.0.txt-   but does not affect existing log message.
--
1.7.0.txt: * "git rebase -i" also learned --autosquash option that is useful
1.7.0.txt:   together with the new "fixup" action.
1.7.0.txt-
--
1.7.3.txt: * "git rebase -i" peeks into rebase.autosquash configuration and acts as
1.7.3.txt:   if you gave --autosquash from the command line.
1.7.3.txt-
--
1.7.4.txt: * "git commit" learned --fixup and --squash options to help later invocation
1.7.4.txt-   of the interactive rebase.
--
1.7.4.txt: * "git rebase --autosquash" can use SHA-1 object names to name which
1.7.4.txt:   commit to fix up (e.g. "fixup! e83c5163").
1.7.4.txt-
1
5

I created some aliases to make it easier to use the git commit --fixup and git commit --squash commands added in git 1.7. Add these to your ~/.gitconfig:

[alias]
  fixup = !sh -c 'REV=$(git rev-parse $1) && git commit --fixup $@ && git rebase -i --autosquash $REV^' -
  squash = !sh -c 'REV=$(git rev-parse $1) && git commit --squash $@ && git rebase -i --autosquash $REV^' -

Usage:

$ git commit -am 'bad commit'
$ git commit -am 'good commit'

$ git add .          # Stage changes to correct the bad commit
$ git fixup HEAD^    # HEAD^ can be replaced by the SHA of the bad commit

The bad commit can be several commits back.

1
  • Using your fixup alias. works great. By adding --autostash, its perfect.
    – kuga
    Commented Oct 22, 2021 at 10:25
4

My current git work flow is so --fixup/--squash intensive, that I wrote a new git-fixup command that handles most of the annoying bits automatically:

  • git fixup shows the modified files grouped under that latest commits that touch the same files
  • git fixup -a commits all those changes as --fixup changes with their corresponding "parent" commits
  • git fixup -r does an automatic git rebase --autosquash for all the fixup commits

A lot of changes are such that just the three commands above are enough to get the job done, no copy-pasting of commit-id's or reading thru the git log to find the right --fixup targets.

Source: https://github.com/ohmu/git-crust

0
2

If you want to squash the last two commits then you have to invoke

git rebase --interactive <3rd last commit>

You then need to pick the last commit and squash the second-to-last commit. You cannot squash the topmost commit of a history.

0

I think the root of the problem is that git (and version control generally) forces you to think in terms of sequences of changes, but a changeset or feature-branch or whatever you call a cohesive group of related changes is in general not logically sequential. The order in which the code was written is incidental and not necessarily related to the order in which it should be read.

I don't have a solution to that, but I have written a Perl script to help automate the process of rewriting history. It's similar to the Python script of @MikaEloranta which I hadn't seen when I wrote it.

commit --fixup and rebase --autosquash are great, but they don't do enough. When I have a sequence of commits A-B-C and I write some more changes in my working tree which belong in one or more of those existing commits, I have to manually look at the history, decide which changes belong in which commits, stage them and create the fixup! commits. But git already has access to enough information to be able to do all that for me.

For each hunk in git diff the script uses git blame to find the commit that last touched the relevant lines, and calls git commit --fixup to write the appropriate fixup! commits, essentially doing the same thing I was doing manually before.

If the script can't resolve a hunk to a single, unambiguous commit, it will report it as a failed hunk and you'll have to fall back to the manual approach for that one. If you changed a line twice in two separate commits, the script will resolve a change on that line to the most recent of those commits, which might not always be the correct resolution. IMHO in a "normal form" feature branch you shouldn't be changing a line twice in two different commits, each commit should be presenting the final version of the lines that it touches, to help the reviewer(s). However, it can happen in a bugfix branch, to contrive an example the line foo(bar()); could be touched by commit A (rename foo to fox) and commit B (rename bar to baz).

If you find the script useful, please feel free to improve and iterate on it and maybe one day we'll get such a feature in git proper. I'd love to see a tool that can understand how a merge conflict should be resolved when it has been introduced by an interactive rebase.

Not the answer you're looking for? Browse other questions tagged or ask your own question.