13

I have two git repositories that are tangentially related. Namely, content of one was a predecessor of the other. I would like to somehow prepend the full history of depository A to the depository B, so that tip of A would be a parent of the very first changeset of repository B? Histories in both are pretty linear.

Is this possible?

Qix - MONICA WAS MISTREATED
  • 14,451
  • 16
  • 82
  • 145
SilentGhost
  • 307,395
  • 66
  • 306
  • 293

1 Answers1

13

You could try using the graft file (.git/info/grafts) where you could overwrite the parenthood of a commit (like the first of projectB having for parent the latest of projectA)

See also "What are .git/info/grafts for?" and "How to prepend the past to a git repository?" for more on this manipulation.


skalee comments about the article "Git: Grafting repositories" (from SO user Ben Straub) for a concrete example.

Now what we want to do is change the first commit in the “nuevo” repo (“New commit #1”) so that its parent is the last commit in the “old” repo (“Old #3”). Time for some voodoo:

git fetch ../old master:ancient_history

Git lets you fetch from any other git repository, whether this repo is related to it or not! Brilliant! This leaves us with this:

enter image description here

Note how we renamed the old master branch to ancient_history. If we hadn’t, git would have tried to merge the two, and probably given up in disgust.

Now we still have a problem.
The two trees aren’t connected, and in fact a git pull won’t even get the ancient_history branch at all. We need a way to make a connection between the two.

Git has a facility called a graft, which basically fakes up a parent link between two commits.
To make one, just insert a line into the .git/info/grafts file in this format:

[ref] [parent]

Both of these need to be the full hash of the commits in question. So let’s find them:

$ git rev-list master | tail -n 1
d7737bffdad86dc05bbade271a9c16f8f912d3c6

$ git rev-parse ancient_history
463d0401a3f34bd381c456c6166e514564289ab2

$ echo d7737bffdad86dc05bbade271a9c16f8f912d3c6 \
       463d0401a3f34bd381c456c6166e514564289ab2 \
       > .git/info/grafts

(in one line, as suggested by ssokolow)

echo $(git rev-list master | tail -n 1) $(git rev-parse ancient_history) > .git/info/grafts 

There. Now our history looks like this:

enter image description here

Cloning this repo results in this:

enter image description here

Woops. It turns out that grafts only take effect for the local repository. We can fix this with judicious application of git fast-import:

$ git fast-export --all > ../export

$ mkdir ../nuevo-complete

$ cd ../nuevo-complete

$ git init

$ git fast-import < ../export
git-fast-import statistics: [...]

(in one line, as suggested by ssokolow)

git filter-branch $(git rev-parse ancient_history)..HEAD 

This effectively converts our “fake” history link into a real one.
All the engineers will have to re-clone from this new repository, since the hashes will all be different, but that’s a small price to pay for no downtime and a complete history.

enter image description here

As Qix comments below:

fast-import appears to just import the git information, but doesn't check anything out.
git init originally puts you on master, so you need a git reset --hard HEAD to actually check the files out after you fast-import.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Worked just fine for me, thanks VonC, didn't think it would be that simple. – SilentGhost Jul 10 '10 at 20:30
  • 1
    Then you can rewrite history using `git filter-branch` to make what graft brought permanent... but this rewrites history. – Jakub Narębski Jul 10 '10 at 21:59
  • Short and neat article which really helped me to understand what grafts are and step-by-step how to join those histories: http://ben.straubnet.net/post/939181602/git-grafting-repositories – skalee Dec 19 '12 at 12:49
  • @skalee good article. I have included the main commands in the answer. – VonC Dec 19 '12 at 12:58
  • Shoot, that was easy. Might want to mention, too, that a `git reset --hard HEAD` needs to happen as the `fast-import` doesn't check out the head of master for you (so instantly `git status` shows that you deleted everything). – Qix - MONICA WAS MISTREATED Jul 14 '15 at 18:12
  • @Qix do you mean a reset --hard just at the end after the fast-import step? – VonC Jul 14 '15 at 18:22
  • @VonC correct. `fast-import` appears to just import the git information, but doesn't check anything out. `git init` originally puts you on master, so you need a `git reset --hard HEAD` to actually check the files out after you fast-import. – Qix - MONICA WAS MISTREATED Jul 14 '15 at 18:25
  • 1
    @Qix Good point. I have included your comment in the answer for more visibility. – VonC Jul 14 '15 at 18:28
  • You might want to suggest people use this command to minimize the chance of copy-paste errors: `echo $(git rev-list master | tail -n 1) $(git rev-parse ancient_history) > .git/info/grafts` – ssokolow Jan 24 '17 at 08:57
  • @ssokolow Thank you. I have included your suggestion in the answer for more visibility. – VonC Jan 24 '17 at 08:59
  • I also just finished checking whether there was a more elegant alternative to using `fast-export` and `fast-import` and the `filter-branch` manpage actually has an example for just this situation. Here's the adjusted version I ran and then tested the output of: `git filter-branch $(git rev-parse ancient_history)..HEAD`. (Reminders: If you have uncommitted changes, you'll need to run `git stash` before and `git stash pop` after. If this isn't your first time running `filter-branch`, you may need to add `-f` to overwrite an old refs backup.) – ssokolow Jan 24 '17 at 09:35
  • @ssokolow nice: you can edit the answer to include that alternative version. – VonC Jan 24 '17 at 09:56
  • Done. I also fixed a typo where you accidentally cut off the "N" in "Now" at the beginning of a quote. – ssokolow Jan 24 '17 at 10:21