Git doesn't push changes, but rather commits. That's because Git doesn't store changes either.
Every commit holds a full, complete snapshot of all of its files. That's the data part of a commit. Every commit also holds some metadata, such as who made it (name and email address), when, and why (the log message). You can view a commit as changes, by comparing the snapshot to the previous commit's snapshot, but it's still a full snapshot.
Every commit has its own unique hash ID. That hash ID means that commit: never any other commit, and no other Git repository anywhere can use that commit hash ID for any other commit. Every Git repository that has this commit uses that hash ID for it. (Which is why commit hash IDs are so big and ugly: they have to be unique!)
Meanwhile, inside the metadata for each commit, a commit holds the raw hash ID of some earlier or parent commit. We say that this commit points to its parent. If we draw a diagram of several commits, we see that they tend to become backwards-looking, linear chains:
... <-F <-G <-H
where H
is the latest or last commit. It has some big ugly hash ID, which I've just called H
, and commit H
contains the actual hash ID of earlier commit G
, which contains the hash ID of yet-earlier commit F
, and so on. These backwards chains make it easy for Git to show you a commit as a set of changes, which is much smaller (and more useful) than just showing you the entire snapshot every time.
What a branch name does in Git is that it lets you—and Git—quickly find this last commit. I'm going to stop drawing the internal arrows as arrows (because of laziness) but keep drawing branch names as arrows:
...--F--G--H <-- master
The name master
contains the hash ID of the last commit H
.
I cloned a repo, changed several files and now I want to push that to the origin master.
Technically, you cloned the repo—which got you all their commits—and made you your own name master
. Your clone also remembered the hash ID of the last commit in their master, under your name origin/master
, which we can draw like this:
...--G--H <-- master (HEAD), origin/master
(This HEAD
thing is how Git remembers which branch name you're using.)
When you made a new commit, it got a new, unique hash ID. Let's just call this I
for short:
...--G--H <-- origin/master
\
I <-- master (HEAD)
Note how your master
now points to commit I
, rather than H
. But you can still find H
in two ways: I
points back to H
, and origin/master
points directly to H
.
Meanwhile, in their repository, perhaps someone has added a new commit or two:
...--G--H--J--K <-- master
Remember, this is their repository (and their master
), not yours.
When you run git push origin master
, your Git calls up their Git. Your Git tells them you have commit I
and you'd like to give it to them. Your Git tells them I
's parent is H
and they say ok, I already have that commit, just give me I
. You send over I
to them:
I [your commit]
/
...--G--H--J--K <-- master
It doesn't matter whether we draw I
above or below, or draw it more branch-ily as:
I [your commit]
/
...--G--H
\
J--K <-- master
as long as we draw in all the relevant commits and their connections.
Now your Git asks them—their Git—to set their master
to point to commit I
, which you both now have. If they obey this polite request, they will have:
I <-- master
/
...--G--H--J--K
They won't be able to find commits J-K
any more, because their Git finds the commits using their branch name. That finds I
, which leads back to H
and then G
and so on, but never forwards.
If you use git push --force
, you change your polite request into a command: Set your master
! If they obey, they'll lose commits J-K
.
What should you do about this?
What you will need, if they have these commits, is some way of sending your commit(s) so that they don't lose theirs.
You have several options. One is to use git merge origin/master
to combine your work with theirs. The other is to use git rebase
, which is more complicated.
Merging
To merge, use git fetch
to make sure your own Git is up to date with theirs, then use git merge origin/master
.1 This makes a new merge commit, which I'll draw in as M
:
I-----M <-- master (HEAD)
/ /
...--G--H--J--K <-- origin/master
What makes a merge commit a merge is that it points back to more than one parent. The two parents here are commits I
and K
. If you now have your Git call up their Git and send your master
, your Git will offer them commit M
. They'll take M
. Since M
has two parents, your Git will offer I
and K
. They have K
but not I
,2 so they'll take I
too, and your Git will offer H
. They have H
so they'll just take M
and I
in the end.
Then, your Git will ask them to set their master
to point to M
. That won't lose anything, so they'll probably accept this request. Your own Git will now update your own origin/master
—your memory of their master
—accordingly.
1If you really like git pull
, you can use git pull
to combine the git fetch
step with the git merge
step. All git pull
does is run both commands. This robs you of the opportunity to insert Git commands between the two commands, and for Git newbies, I find this makes things more confusing, so I prefer to keep these as separate commands.
2They might still have I
from your earlier attempt, or not, depending on their Git version ... and whether you made an attempt after all. If they do have I
, that's fine: I
's hash ID is unique to that commit.
Rebase
What git rebase
does, in a nutshell, is to copy some commits to new-and-improved versions, and then move a branch name.
Nothing—not you and not Git itself—can change any existing commit. All commits are frozen for all time. That's what makes the hash IDs work. But you can take a commit out, and work on it, and make a new commit that's just as good as the original in terms of what changes it makes, and better than the new commit in that it goes in the right place.
That is, suppose you have:
I <-- master (HEAD)
/
...--G--H--J--K <-- origin/master
What if there is an easy way to have Git copy existing commit I
to a new-and-improved commit that comes after commit K
?
There is, and it's git rebase
. We run:
git rebase origin/master
and Git lists out all the commits on our master
(I
, H
, G
, ... on back) and then strikes out from this list all the commits that are on origin/master
(K
, J
, H
, G
, ...). That leaves commit I
. (If we had more commits, such as I-L
, we'd have that list here, and the next step would copy two commits instead of one.)
Our Git then uses detached HEAD mode (which we'll just gloss over) to copy each commit, one at a time, to come after commit K
, the one we named with origin/master
:
I <-- master
/
...--G--H--J--K <-- origin/master
\
I' <-- HEAD
Having copied all the commits, our Git then yanks our name master
over here, to the last-copied commit, and re-attaches HEAD
to master
:
I [abandoned]
/
...--G--H--J--K <-- origin/master
\
I' <-- master (HEAD)
Since commit I
does not have a name, we won't find it directly, and since I'
points to K
which points to J
which points to H
which points to G
, we won't find it that way either. It will seem as though we somehow altered existing commit I
.
We didn't—I'
has a totally different hash ID from I
—but since we're the only Git that had I
, nobody else needs to know this. We can now run:
git push origin master
to send commit I'
to the other Git, and ask them to set their master
to point to I'
. As long as their master
still points to K
right now, this will succeed.