11

I am in some software project in which we store our code on GitHub. There's a repo with master and development branch. I was on my laptop without an internet connection and I wanted to start working on another feature, so I did the following:

git init
git remote add origin git://repo_adress
git checkout -b webapp <- created a new branch

I've added few files, folders, etc.

Then I did:

git add .
git commit -m "something"
git push origin webapp

I checked on GitHub, now there are 3 branches. I want to merge webapp to development (there will be no conflicts, I work in a separate folder). Unfortunately when I tried to git pull, this happened:

 * [new branch]      development -> origin/development
 * [new branch]      master      -> origin/master
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

Now when I go with git pull origin development, this shows up:

From github.com:repo_address
 * branch            development -> FETCH_HEAD
fatal: refusing to merge unrelated histories

What can I do now? Should I rebase?

2 Answers2

40

TL;DR

You should probably create new branch and cherry-pick your commit into it. You can merge with --allow-unrelated-histories. Either way you need at least one new commit, to tie into the existing branches.

Long

This part is the key to the problem:

I was on my laptop without an internet connection ...

You made, on your laptop, a repository that has no relationship with the other repositories.

git init

I assume you did this in an empty directory, so that it actually created a new, empty repository. Your new repository has no commits, and therefore no branches, even though you are (contradictorially) on branch master. That is, you're on master, but master doesn't exist!

git remote add origin git://repo_address

This adds a remote but does not contact the remote (which of course would have been impossible). As a result, your repository continues to have no commits.

git checkout -b webapp   <- created a new branch

In fact, this has not yet created the branch. All it has done is change you from being on the nonexistent master branch to being on the nonexistent webapp branch.

Then I did:

git add .
git commit -m "something"

This is where the branch actually got created, when you made, in this empty repository, its very first commit. And, since this is the first commit in the repository, it has the special property that it has no parent commit. Git calls this a root commit.

git push origin webapp

This, of course, requires Internet access, because it calls up the Git at the URL you supplied. However, as long as you have access, what this does is transfer the commits you made—the single root commit—intact, with all its parents (it has none) and all its files, and ask the other Git, at repo_address, to change or create its branch name webapp to match the last commit on your own webapp.

Everything is OK so far, but now you have a repository with two roots

The Git repository at repo_address now has a commit graph that looks like a much more complicated version of this:

A--...--M   <-- master
     \
      N   <-- development

O   <-- webapp

I've assumed just a few commits (so that I can number them with single letters rather than big ugly hash IDs). Commits A and O are root commits. All other commits descend from A or O (but nothing actually descends from O, unless you made more than one commit on your laptop).

Unfortunately when I tried to git pull, this happened:

 * [new branch]      development -> origin/development
 * [new branch]      master      -> origin/master
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

The git pull command just runs git fetch followed by a second Git command. In this case, it ran git fetch origin with no additional constraints (because there's no upstream set in your repository for your webapp), so your Git called up the other Git, the one at repo_address, and downloaded all the commits and branches they have that you don't—which at this point, is the master and development work and those commits. Now you have the same graph that they have, except that your Git uses remote-tracking names rather than branch names:

A--...--M   <-- origin/master
     \
      N   <-- origin/development

O   <-- webapp (HEAD), origin/webapp

Note that you still have your own branch name, and both names—your branch name webapp, and your remote-tracking name origin/webapp that remembers their branch name webapp—point to commit O, your new root commit.

The second command that git pull runs is normally git merge. This merge needs an upstream setting if you haven't specified what to merge, and since you did not have an upstream and did not specify what to merge, this part just failed.

Then you ran:

git pull origin development

which specified what to merge: the commit identified by their development, that your own Git is remembering via the remote-tracking name origin/development. (I drew this as commit N, in my guess-at-a-graph.)

The git merge command works by finding a shared commit. This is the "nearest" (whatever that means exactly: it's kind of intuitive from a graph drawing) commit that's on both branches. One of the two branches is your current branch, i.e., the one your HEAD is attached to. The other branch is the one found from the commit you named, i.e., commit N. So Git walks from commit N back towards its root, commit A, to find the appropriate shared commit. It also walks back from commit O towards its root, to find the shared commit. But O is a root, so the walk backwards from O stops at O. There is no shared commit!

This is why Git complains. The history attached to commit O consists of one commit, which is commit O itself. There's much more history behind commit N, but it never leads to commit O. With no shared history, merging does not make sense.

Cherry-picking

Cherry-picking consists of turning a commit—which is a snapshot—into a change-set to see "what happened" in that commit, then applying that same change-set to some other commit. A change-set is just a list of diffs: instructions for turning one commit into another commit. That's what git diff shows, for instance. To turn a commit into a diff, Git compares the commit to its parent.

Clearly, cherry-picking a root commit is a little tricky, but Git can do it. What it does is to compare the root commit to an empty commit (well, more precisely, an empty tree). The resulting diff consists of commands reading: Add this new file; here are the contents.

So you can now check out your existing branch name development, except for one problem: you don't have an existing branch named development. Remember, you have this:

A--...--M   <-- origin/master
     \
      N   <-- origin/development

O   <-- webapp (HEAD), origin/webapp

But git checkout will create a branch named development for you, if you ask it to check out development, because it will find that there's origin/development that looks enough like development that it will go ahead and create the local name, pointing to the same commit, and then make it your HEAD:

A--...--M   <-- origin/master
     \
      N   <-- development (HEAD), origin/development

O   <-- webapp, origin/webapp

You can now create another new branch, webapp2 for instance, that also points to commit N, using git checkout -b webapp2:

A--...--M   <-- origin/master
     \
      N   <-- development, origin/development, webapp2 (HEAD)

O   <-- webapp, origin/webapp

and now you can merge or cherry-pick commit O.

Branch names vs branches

I'll outsource most of this section to another StackOverflow question: What exactly do we mean by "branch"? When we talk about "branches" here, if we don't mean branch names, we mean the series of commits ending in the branch tip commits like M and N. You will want your work to relate (in branch-graph-structure or branch-ancestry terms) to these existing branches. That means you want your new commit(s) to link back to commit N, the current tip of development.

If you cherry-pick

Suppose you create webapp2 as above, then run git cherry-pick webapp. Git will copy commit O to a new commit O' that applies to the current commit N. Git will do this by executing all those "create new file with these contents" operations that the git diff produced by comparing commit O to an empty tree. Since the new files don't interfere with any existing files, this should all just work.

When Git makes the new commit, it will drag the current branch webapp2 forward to accommodate it, and you will have:

A--...--M   <-- origin/master
     \
      N   <-- development, origin/development, webapp2 (HEAD)
       \
        O'   <-- webapp2 (HEAD)

O   <-- webapp, origin/webapp

which is probably what you wanted all along. Now you can delete your own webapp, then delete webapp in the other Git at repo_address, and just use webapp2 everywhere, as if you had done this the way you had intended, and would have if you'd had Internet access earlier.

If you merge

You can, instead, run git merge --allow-unrelated-histories webapp. This will tell Git to make two diffs, using that same empty tree as the common starting point. One compares to the contents of commit N: Add every file. One compares to the contents of commit O: Add every file. Git then combines the operations—which are all on different file names—and makes a merge commit, with two parents:

A--...--M   <-- origin/master
     \
      N   <-- development, origin/development, webapp2 (HEAD)
       \
        P   <-- webapp2 (HEAD)
  _____/
 /
O   <-- webapp, origin/webapp

Once again, you can delete webapp everywhere. The weird thing is that you now have, in your history (your set of commits), this extra root commit O.

Exercise: git merge --squash

Look up what git merge --squash does and predict how it would leave your repository. How does this compare to cherry-picking? (Try it in a clone, if you like.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Fantastic answer. Thanks! I was trying to figure out why my repo had multiple roots, and this explains it exactly. – Lemur Oct 16 '18 at 22:26
  • Is it possible to run `git merge --allow-unrelated-histories foo` but not bring any of the changes of foo in the merge? The merge would end with the branch we are merging into being untouched, having the same file state. Is that possible? – trusktr May 02 '20 at 17:52
  • 1
    @trusktr: if you want the snapshot of the new merge to exactly match the snapshot in the branch you're in, use `git merge -s ours`, the "ours" *strategy* (not the "ours" eXtended-strategy-option). This probably doesn't even require the `--allow-unlreated-histories` flag since it's the strategy itself that must determine the merge base. – torek May 02 '20 at 19:22
0

Simple rebase might also not work because it also tries to find the "last common commit".

Best chance is with the tree operator form

rebase --onto target_branch start_commit_exclusive end_commit_inclusive

where target_branch is the remote master, start_commit_exclusive ist the first commit in your unrelated branch and end_commit_inclusive is the HEAD of your unrelated branch.

Problem is that you cannot include your "initial" commit of your unrelated branch so you ma *cherry-pick" it first into the remote master:

git checkout -b master origin/master
git cerry-pick SHA_OF_YOUR_FIRST_COMMIT
git rebase --onto master  SHA_OF_YOUR_FIRST_COMMIT webapp
Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51