Reputation: 187
Say I work with two computers: Say, I work on a project from the home computer but the previous day I forgot to push the latest modifications from my work computer.
What happens in case of push/pull? If I push on the computers, will I end up with two branches? How to solve the conflict?
Upvotes: 3
Views: 1799
Reputation: 489293
In this case, it's exactly the same as if you and Bob (or Carol or anyone-else-not-you) are both working in repos you both cloned from a shared location, and one of you pushes changes back before the other. The first one to push "wins".
Git does not know or care who you are.1 It just looks to see, on a push, whether the push is a "fast-forward" operation. If so, the push is allowed by default.2 When whoever is first—let's say Bob wins—when Bob does his push, the new commits he has are stacked directly on top of the tip of the branch of the repository receiving the push. That's a fast-forward, so it's allowed. Then you attempt your push, but your new commits are stacked along side some commits now in that repo:
D-E <-- branch (updated by Bob)
/
...-A-B-C
\
F-G <-- what you're trying to push
[Note: this is what the remote sees, i.e., the remote has branch
pointing to E
. You have branch
pointing to G
and you don't have D
and E
in your repository. By doing a push, you give the remote F
and G
and then ask it to change its idea of where branch
points.]
You're asking the remote to set branch
to point to G
. If the remote does that, commits D
and E
get lost, so it just tells you no, not unless you force the push.
This is true even if you are also Bob (on your other computer).
In this situation, you need to bring over the commits that Bob pushed:
$ git fetch
This will bring over commits D
and E
, in this particular case. Once you have those, you then need to modify what you want to push to somehow incorporate D
and E
into your F
and G
.
There are two easy ways to do this, using git merge
and git rebase
.
At this point you can usually simply run:
$ git merge
(since there will be an upstream that git knows about). What git merge
does is attempt to combine a forked development and make a new "merge commit" that points back to both chains:
D-E
/ \
...-A-B-C M <-- branch
\ /
F-G
[Note: here and below, this is what you have in your local repository.]
If the automated merge goes well, you should make sure the results are good, because git is not magic, it just follows a bunch of rules for doing merges. If the automated merge goes poorly, you must assist git since its rules didn't do the trick.
If you push this, you ask the remote to set branch
to point to M
. While M
doesn't exactly stack directly on top of E
, it does point back to E
as one of its parents, so M
is a (slightly complicated) fast-forward. (In fact, M
simply needs to have E
as some ancestor, not just a direct ancestor but anywhere in its history. The key is that no commits will be lost.)
The advantage to an actual merge is that it shows what really happened: Bob won the push race, and then you adjusted your stuff to match. The disadvantage is that it's hard to back-track stuff through a merge: if there's a bug in Bob's changes, or a bug in yours, it's harder to tell precisely where the bug went in; the history "looks complicated", and the complications are often not really useful. So this is why people use the second alternative.
Instead of git merge
, you can usually at this point simply run:
$ git rebase
(for the same reason that you can run git merge
with no extra arguments: git usually knows about the upstream and can figure out what to rebase). What this does is attempt to "copy" your commits, in this case F
and G
, into slightly different versions that simply stack on the end of the latest fetched commit, in this case E
. If all goes well, the result looks like this:
D-E
/ \
...-A-B-C F'-G' <-- branch
\
F-G [to be abandoned]
Your original F
and G
are still in there (and are available under the name ORIG_HEAD
until another rebase or whatever overwrites ORIG_HEAD
; they also stay in the reflogs for 30 days by default, if you need to recover them) but are largely forgotten-about if the rebase goes well.
As with the merge case, you need to make sure the rebase really worked: even if git thinks it all worked fine, it's just following simple rules, just as for merge.3 But usually it does just work, and the original F
and G
can be ignored, so we can redraw the commit graph like this, calling the copies F
and G
now:
...-A-B-C-D-E-F-G <-- branch
If you now try pushing this to the remote, it sees that you're just stacking two commits on top of what was the last commit: that's a fast-forward, so it's allowed.
git pull
?The pull
script is really just a convenience thing for doing git fetch
followed by git merge
or git rebase
. It literally invokes git fetch
(in a slightly convoluted way that, in older versions of git, prevents some useful stuff from happening, though the details are not very important here). When the fetch finishes, it literally invokes git merge
unless you've told it to use git rebase
. If you have told it to use rebase
you can also tell it whether to invoke git rebase
with --preserve-merges
. In all cases, it invokes the merge or rebase in a slightly convoluted way. Whether and when to use --preserve-merges
is beyond the scope of this answer, though.
In older (but not too much older) versions of git, if you have git pull
do the rebase, it is also able to recover from an upstream rebase where a regular git fetch; git rebase
sequence can't. In newer versions of git, rebase
uses the new --fork-point
stuff to figure this out on its own, so that there's no big advantage to the pull
script any more.
git pull
?I used to avoid it because it had a bunch of bugs in it. I think they're mostly fixed now so I avoid it mostly from habit. Feel free to use it, just remember it's just short-hand for fetch
then merge
-or-rebase
. Decide whether you prefer merges or rebases, and set your "rebase on pull" settings accordingly. As usual with git, it's overly complicated. Or just avoid git pull
, like I do, so that you don't have to figure out what all the branch.autosetuprebase
values mean.
1The method by which you push usually does care, e.g., using ssh and keys. But this authentication happens before git is involved; once git is running, it does not care because it does not have to care.
Even when you create a commit, git just uses the name and email you have configured. You can set these to anything at any time, so in that particular sense it still doesn't care who you are.
2The precise rules for what is and is not allowed are up to whoever runs the system you push-to. The "default default" rules, i.e., what you get if you set things up and don't change anything, is that a push to a branch is allowed if it is a fast-forward or if the force flag is set. A push to a tag is allowed if the tag does not exist or if the force flag is set. (Some older versions of git apply branch rules to tags too, but newer ones are more strict.)
3In fact, all the work is done by common code, the "merge engine" inside git. This is used for pretty much everything: as long as git can find a common ancestor in the commit graph, it can do a 3-way merge.
Upvotes: 6