19

I imported a Bazaar repository into Git (using git bzr), but the resulting repository contains a spurious commit parent link:

Spurious parent link

Notice that the commit tagged 1.02-6 is based off the 1.02-3 commit, but 1.02-1 is unnecessarily also marked as a parent. (Note: All the commits in this part of the repo are tagged; there are no commits between the ones shown.)

I have tried rebasing in several ways (on the master branch: git rebase 1.02-3, git rebase -i upstream-1.02, git rebase --onto 1.02-1 1.02-3, git rebase --root upstream-1.02 --onto=other_branch), but in each case it fails with a merge conflict. These seem to be attempting more than is necessary; the history is correct except for an extra parent pointer being recorded in the commit tagged 1.02-6.

How do you remove the link in order to linearize the history? Is there a better way than manually cherry-picking all the commits in sequence?

Mechanical snail
  • 29,755
  • 14
  • 88
  • 113
  • 1
    Is that really "spurious"? Or were there bug fixes made to `1.02-1` that were merged into `1.02-6` but didn't make it into `1.02-3` (or made it there via a different path)? Not that familiar with `bzr`, and I'm assuming the graph above is simplified to tagged releases only (i.e. there are other commits/changesets between them that just aren't shown at the moment). – twalberg Jan 09 '13 at 20:13
  • @twalberg: Not simplified. Those are all the commits. – Mechanical snail Jan 09 '13 at 20:19
  • Might be useful: http://git-scm.com/docs/git-commit-tree, `git cat-file -p 1.02-6` – Mechanical snail Jan 11 '13 at 22:50

4 Answers4

22

The easiest way to do this (in git >= 1.6.5) is to use:

git replace --edit <sha>

and remove/add/change the Parent: lines.

Once you are happy the change is right, you can rewrite the commits to make the change permanent:

git filter-branch --tag-name-filter cat -- --all

In some cases it'll be noticeably quicker to only rewrite the commits involved and not the full history (thanks to Michael for mentioning this in the comments); e.g. to rewrite only commits on the current branch:

git filter-branch --tag-name-filter cat -- <new parent sha>..head

If you're not sure, use --all, otherwise you risk ending up with other branches/tags still referencing the temporary replacement object.

JosephH
  • 37,173
  • 19
  • 130
  • 154
  • 1
    Simple and direct to the point without multiple error prone commands. This should be the accepted answer! – L. Holanda Sep 20 '16 at 21:14
  • 3
    Note we can run the second command as `git filter-branch --tag-name-filter cat -- ..head` so we just rewrite only the commits needed and not every single commit. The arguments supported are documented here https://www.kernel.org/pub/software/scm/git/docs/git-rev-list.html – Michael Feb 07 '17 at 01:56
  • 2
    Thanks, that's a clean way to rewrite a merge commit (2 parents) as a regular commit (1 parent). The addition from Michael to not rewrite the full history proved very useful in my case. I will suggest an edit for the answer – floribon Mar 28 '18 at 06:17
  • If you ran `git replace --edit ` on the latest commit, you don't need the second command (`git filter-branch ...`) – Antônio Medeiros Jul 26 '19 at 03:28
  • Is the `` in the 2nd command referring to the sha of the original parent? or the new parent? – Jorge Luque Jun 05 '20 at 22:50
  • 1
    @JorgeLuque It would be the new parent, but beware that command only rewrites the current branch. If you're not sure it's best to use `--all` (a stack overflow user had unhelpfully edited my answer to remove all mention of `--all`, I've reverted that edit now). – JosephH Jun 07 '20 at 08:25
  • @AntonioViniciusMenezesMedei That is not correct. Even if you use `git replace --edit` on the latest commit, it will still create a replacement object in `/refs/replace` (or a graft, depending on git version), and this will behave differently from a permanent full replacement in some cases. `git filter-branch` is necessary to turn the replacement object into a normal object. – JosephH Jun 07 '20 at 08:32
7

You can do it manually using the git commit-tree internal command.

We want to edit the commit tagged 1.02-6 to remove the spurious parent pointer (to 56a2f3b5948ab54c9239c2b384a6ea9eb1f410c4).

First, read the information from the existing commit object:

user@host:/path/repo.git$ git cat-file -p 1.02-6 
tree c658aa1ebcf2bf2a607696c7868b875be72fb01f
parent 56a2f3b5948ab54c9239c2b384a6ea9eb1f410c4
parent 4e671bf1d2298729c9e5cfd8229051cfe2c40831
author James Damour (Suvarov454) <suvarov454@users.sourceforge.net> 1146319620 -0400
committer Bazaar Package Importer <james.westby@ubuntu.com> 1146319620 -0400

The "main/" in the Section line of debian/control should be assumed.

Extract the commit message using git log --format=%B -n 1 1.02-6.

Now create a new commit with the same content (excluding the spurious parent link, and the committer info):

git log --format=%B -n 1 1.02-6 | \
    GIT_AUTHOR_NAME="James Damour (Suvarov454)" \
    GIT_AUTHOR_EMAIL="suvarov454@users.sourceforge.net" \
    GIT_AUTHOR_DATE="1146319620 -0400" \
    git commit-tree c658aa1ebcf2bf2a607696c7868b875be72fb01f \
        -p 4e671bf1d2298729c9e5cfd8229051cfe2c40831

This created a new commit, and printed its hash (cc32e66...). Now turn it into a new branch:

git checkout -b fixed_commit cc32e66

and rebase master onto the new branch:

git checkout master
git rebase fixed_commit

And we're done:

Finished

You probably want to delete the old branches and re-tag the appropriate commits.


Actually it might be easier to use git filter-branch --parent-filter. I haven't tried that.

Community
  • 1
  • 1
Mechanical snail
  • 29,755
  • 14
  • 88
  • 113
2

This will correct the parents without changing anything else (eg. committer dates):

git filter-branch --tag-name-filter cat --parent-filter 'test $GIT_COMMIT = [sha of 1.02-6] && echo "-p [sha of 1.02-3]" || cat' -- 1.02-1..master

You will have to replace the bracketed text with the appropriate commit IDs. If you have more downstream branches that need to be rewritten, change 1.02-1..master to --all and be prepared to wait.

Of course, don't use this or any other solution if others have branched from any commits after the ones you want to edit. They will hate you.

hypehuman
  • 1,290
  • 2
  • 18
  • 37
  • If you don't need to care about others (for example in the case of a cleanup right after a conversion from svn or bzr to git and before you pushed to your git server) then this is the best answer, really. I tried all of them. – Amedee Van Gasse Dec 30 '14 at 13:05
1

You could try a rebase. There is an example a bit down (search for --onto) that I think is similar to your case.

I think you need to do

git rebase --onto 1.02-1 1.02-3

which should put everything after 1.02-3 onto 1.02-1 and that is probably what you want.

Remember that the hashes will be different on everything from the first changed commit but I assume you are doing this as a first step in moving from bzr so no-one else should have cloned this yet.

Andreas Wederbrand
  • 38,065
  • 11
  • 68
  • 78