l0b0
l0b0

Reputation: 58958

Conflict when squashing linear commit history

How is it possible that when trying to squash/fixup a linear branch I still have to do manual merges? The repo has been converted from Subversion. Every conflict is either "Automatic cherry-pick failed" or "Aborting commit due to empty commit message". The latter I could understand, but a --fixup-empty or something would be useful.

Typical output:

user@machine:/path (master|REBASE-i)$ git add * && git rebase --continue 
[detached HEAD c536940] fixup!
 Author: John Doe <[email protected]>
 2 files changed, 57 insertions(+), 4 deletions(-)
Automatic cherry-pick failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>', and
run 'git rebase --continue'
Could not apply 8854a54... >6d5f180 foo
user@machine:/path (master|REBASE-i)$ git st
# Not currently on any branch.
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#   both modified:      filename.ics
#
no changes added to commit (use "git add" and/or "git commit -a")

Upvotes: 5

Views: 2358

Answers (3)

scottgwald
scottgwald

Reputation: 609

I ran into this same puzzling question. I wanted to turn the changes from a whole bunch of sequential commits into a single commit, but using interactive rebasing with a sequence of squashes was frustratingly giving merge conflicts. This was a while back, and I was not able to reproduce it in a minimal example just now. In any case, I do know how to solve it, so here you go:

I'll present two different ways of doing what the post asked -- one that is clunky but "less scary," because it is clear exactly what you're doing, and a second that is elegant and uses only git commands, but requires you to trust your understanding of git a bit more.

Setup

Let's suppose you have a commit ID idA as the initial state and commit ID idB as the final state, with a bunch of other commits in between. The fact that you want to squash all the in-between commits says that you no longer care how you got from the initial state to the final state -- you just want a commit that moves you from point idA to point idB.

Let's suppose that idB corresponds to the current HEAD and say that it's also your master branch. We're going to make a new branch called squashed that has the same working tree contents as master, but has just one single commit after commit ID idA.

Solution 1

  1. git checkout master (you might already be on master)
  2. Use bash or your file explorer to copy the whole working tree to somewhere else. Just make sure you leave out the .git directory -- which is the default behavior on most operating systems. So, to be clear -- open the repo folder, select all, copy, and paste it to some new folder, on your desktop or wherever you want.
  3. git checkout -b squashed idA to make a new branch called squashed with idA as its latest commit, and make it your current branch.
  4. Paste the contents of the master (that you put somewhere else in Step 2) back into the repo folder. If your file explorer asks, tell it to replace all the files that have changed.
  5. In the top level of your repo, git add . and then git commit. Your new commit will have all the changes that take you from idA to idB.

Solution 2

git branch squashed idA # make a new branch called `squash` with `idA` as its latest commit 
git checkout master # master is "idB" in this case.
git symbolic-ref HEAD refs/heads/squashed
git commit

And voilà. When you use symbolic-ref to move the HEAD to the tip of the squash branch, the set of diffs between the working tree state and the new HEAD location are computed and staged.

I want to respond to the concern that some have expressed that this is a "dangerous", "low-level" git hack. I will try to explain just enough about how it works that you can feel at ease. Your .git folder is where all the versioned file contents live. Two of the folders in it are objects, and refs. The refs folder contains files that tell git the names of your branches and what commits they correspond to. So if you open up .git/refs/heads/master, for example, you'll see the commit ID of the latest commit to master. The objects folder contains a bunch of files that are in subfolders with two-character hex names. The objects can be several different things including patches, commits, and whole files. Also in the top level of the .git folder is an index file. This is a great post on the contents of the index file. What you need to know for the present discussion is that the index file tells you what object (in the objects folder) corresponds to the latest committed version of each file on the current branch and commit.

Against that background, here is what the solution does: the symbolic-ref command simply tells git "suddenly" that you're on the squash branch instead of the master branch without touching the working tree. That means that your files all correspond to the state of master you know and love, but git is seeing this as idA with a bunch of uncommitted changes (that is, the index file specifies that the current checked out version of all the files are those of idA, and this is the reference against which working tree changes are measured). The exact changes, in fact, that get you to state idB. This is exactly what Solution 1 does as well. In this case, because of the way symbolic-ref is implemented, all of the said changes are already staged (i.e. git add-ed), so all you have to do is git commit. There is nothing "dangerous" about this, because it doesn't change master or any of the objects that git is keeping track of. You've just created a new file in the refs folder called squashed, and one new file in the objects folder corresponding to that new combined commit. Your master is right there where you left it, you just have a new branch called squashed with the same contents but a shorter commit history. If you aren't sure what's going on, rest assured you can git checkout master and it will be right there where you left it.

If you want to adopt squashed as your new master, you can go ahead and

git branch -m master old-master
git branch -m squashed master

And then you'll have the old branch with all the superfluous commits saved as old-master, and master will be just what you wanted.

Hope this helps!

Upvotes: 0

Beau
Beau

Reputation: 11378

Here's my suggestion for achieving your idea of having some kind of --fixup-empty functionality:

git filter-branch --msg-filter "sed 's/^$/Unknown/'"

This replaces empty commit messages with 'Unknown' and is particularly useful if you want to do a rebase after using git-svn to convert a Subversion repository that had some empty commit messages but can't because it fails with "Aborting commit due to empty commit message".

Upvotes: 2

l0b0
l0b0

Reputation: 58958

These work:

git mergetool
git rebase --continue

Upvotes: 2

Related Questions