You already fixed (?) this using --allow-unrelated-histories
and there's no real reason not to just leave that. But in case you are still wondering...
What happened, and how you (probably) got here
The first key to using Git is to understand that Git is all about commits. Of course, this also requires that you understand, in a reasonably deep sort of way, what a commit is. What it is, is pretty short and simple: it's a permanent (mostly) and immutable (entirely) snapshot plus some metadata. The snapshot contains all of your files—well, all of them as of the time you made the commit—and the metadata has:
- your name and email address, and the time you made the commit;
- your log message, i.e., why you made this commit; and, crucially,
- the hash ID of the commit that comes just before this commit, defined as the parent of this commit.
Every commit is unique—for many reasons, including the time-stamp mentioned above—and every unique commit gets a unique hash ID. That hash ID, some big ugly string of hexadecimal characters, seems random, but is actually a cryptographic checksum of the contents of the commit. It's also the True Name, as it were, of that commit: that ID means that commit, and only that commit. No other commit will ever have that hash ID. That hash ID will always mean that commit.
Git actually finds the commit by hash ID. So the hash ID is crucial. Of course, it's also impossible for humans to remember. So Git gives us a way to remember the latest hash ID, and that way is a branch name like master
or dev
.
The name only has to remember the last commit because each commit remembers its parent's hash on its own. That is, given a tiny repository with just three commits, where we replace the actual hash IDs with a single uppercase letter, we can draw this:
A <-B <-C <-- master
The name master
remembers the hash ID of C
. C
itself—the actual commit, retrieved by hash ID—remembers the hash ID of B
, and B
itself remembers the hash ID of A
.
When something remembers the hash ID of some other commit, we say that this something points to the commit. So the name master
points to C
, C
points to B
, and B
points to A
. Those three commits—C
, then B
, then A
—are the history in the repository.
Note that A
doesn't point anywhere. It literally can't, because it was the first commit. There was no earlier commit. So it just doesn't, and Git calls this a root commit. All non-empty repositories have to have at least one root commit. Most, probably, have exactly one ... but yours has two.
Normal branching and merging
Let's take a quick look at the more normal way to make branches. Suppose we have just these three commits, pointed-to by master
. We ask Git to make a new branch name, pointing to the same commit as master
:
A--B--C <-- dev (HEAD), master
Both names identify commit C
, so commit C
is on—and is the tip commit of—both branches, and all three commits are on both branches. But now we make a new commit. The process of making a new commit—with the usual edit and git add
and git commit
—makes a new snapshot, adds our name and email and timestamp and so on, uses the current commit C
as the saved hash, and builds the new commit. The new commit gets some big ugly hash ID, but we'll just call it D
:
A--B--C <-- dev (HEAD), master
\
D
Since D
's parent is C
, D
points back to C
. But now the magic happens: Git writes D
's hash ID into the current branch name—the one HEAD
is attached to—so now we have:
A--B--C <-- master
\
D <-- dev (HEAD)
and voila, we have a new branch. (Well, we had it before, pointing to C
. Most people don't like to think about it that way though, they want to call D
the branch. In fact, the branch is D-C-B-A
!)
Over time we add some commits to both branches:
A--B--C-----J----K----L <-- master
\
D--E--F--G--H--I <-- dev
We git checkout master
and git merge dev
. Git will find the merge base commit for us, where dev
and master
diverged. That's obviously commit C
since that's where the two branches rejoin in the past. Git will compare C
vs L
to see what we changed on master
, compare C
vs I
to see what we changed on dev
, and combine the changes. Git applies the combined changes to the snapshot in C
—to the merge base—and makes a new merge commit M
, which goes on our current HEAD
branch as usual, updating that branch name so that master
points to M
:
A--B--C-----J----K----L--M <-- master (HEAD)
\ /
D--E--F--G--H--I <-- dev
What's special about M
is that it has two backwards links: it goes back to L
, as all commits would, but it has a second parent I
, which is the current tip commit of branch dev
. Other than the two parents, though, it's quite ordinary: it has a snapshot as usual, and our name and email and timestamp and a log message.
Abnormal branching
There's nothing in Git that stops you from making extra root commits. It's just a little bit tricky. Suppose that, somehow, you did this:
A <-- master
B--C--D--...--L <-- dev (HEAD)
Once you have this situation, git checkout master; git merge dev
just gives you an error. That's because the usual method of finding a merge base—starting at the two branch tips and working backwards—never finds a common commit.
Adding --allow-unrelated-histories
tells Git to pretend that there's a special empty commit before both branches:
A <-- master (HEAD)
0
B--C--D--...--L <-- dev
Now Git can diff 0
vs A
to see what you changed on master
, and 0
vs L
to see what they changed on dev
. On master
, you added every file. On dev
, you also added every file. As long as those are different files, the way to combine those two changes is to add the master
files from commit A
to the dev
files from commit L
, apply those changes to the empty null commit, and commit the result, with parents going back to both A
and L
:
A---------------M <-- master (HEAD)
/
B--C--D--...--L <-- dev
How you (probably) got here
There's a git checkout
option, git checkout --orphan
, that sets this state up. But that's probably not what you did. The state this sets up is the same state you're in when you create a new, empty repository with git init
:
[no commits] <-- [no branches]
There are no branches, and yet Git will say that you're on branch master
. You can't be on master
: it doesn't exist. But you are, even though it doesn't exist. The way Git manages this is that it puts the name master
into HEAD
(.git/HEAD
, actually) without first creating a branch named master
. It can't create the branch because a branch name has to contain a valid hash ID, and there aren't any.
So, when you run git commit
, Git detects this anomalous state: that HEAD
says master
but master
doesn't exist. That's what triggers Git to make our root commit A
. Then Git writes A
's hash ID into the branch, which creates the branch, and now we have:
A <-- master (HEAD)
which is just what we wanted.
But suppose, while we're in this weird no-commits-yet state, we run:
git checkout -b dev
This tells Git: Put the name dev
into HEAD
. It does that without complaint, even though there's no master
either. Then we make our first commit, but for no obvious reason, we'll pick B
as its one-letter stand-in for its hash ID:
B <-- dev (HEAD)
Meanwhile, having run git init
here, then git checkout -b dev
, then done something and git commit
, we'll go over to $WebHostingProvider—whether that's GitHub or GitLab or Bitbucket or whatever—and use its make me a new repository clicky buttons. Those usually have an option: create an initial commit with README and/or LICENSE files and such. If that option is checked—or the don't option is unchecked—they go ahead and make a first commit and a master
:
A <-- master (HEAD)
Now you connect your repository to theirs and have your Git download any commits they have that you don't:
A <-- origin/master
B <-- dev (HEAD)
You can now proceed to add lots of commits, never noticing that your dev
branch is not related to their master
branch (which your Git is calling origin/master
).
Later, you run:
git checkout master
Your Git notices that you don't have a master
, but that you do have an origin/master
. So your Git creates a master
for you, pointing to the same commit as origin/master
, and attaches your HEAD
to your new master
:
A <-- master (HEAD), origin/master
B--C--D--...--L <-- dev
and voila, you're in the pickle you were in.