My workflow goal is often to avoid a meaningless merge. To accomplish that I often do one of two things:
git pull --rebase
to prevent an unneeded merge. If I later wish to change an existing commit I make changes, commit and merge them in using git rebase --interactive
git stash
my changes and then git pull
normally and simply git stash pop
these changes later when I wish to modify and /or commit them.Some colleagues warned me that git stash save
/ git stash pop
are unsafe.
I am wondering if there are any subtle advantages to using commits and git pull --rebase
vs. the stash list.
The git pull command first runs git fetch which downloads content from the specified remote repository. Then a git merge is executed to merge the remote content refs and heads into a new local merge commit.
git fetch is the command that tells your local git to retrieve the latest meta-data info from the original (yet doesn't do any file transferring. It's more like just checking to see if there are any changes available). git pull on the other hand does that AND brings (copy) those changes from the remote repository.
If you have uncommitted changes, the merge part of the git pull command will fail and your local branch will be untouched. Thus, you should always commit your changes in a branch before pulling new commits from a remote repository.
When comparing Git pull vs fetch, Git fetch is a safer alternative because it pulls in all the commits from your remote but doesn't make any changes to your local files. On the other hand, Git pull is faster as you're performing multiple actions in one – a better bang for your buck.
Using git stash
avoids the "meaningless merge" only if you have no commits of your own. The git pull --rebase
method is more general.
Using git stash
just makes some commits,1 so to a first approximation, stashing and committing are basically the same. The stash commits take the form of what I like to call a "stash bag", which is hung off the tip-most commit of whatever branch you're on when you do the stash
.
The first difference is that the stash-bag commits do not move the tip of the branch. That is, suppose the branch (let's call it devel
) looks like this:
... <- E <- F <-- devel
then after stashing, it looks like this:
... <- E <- F <-- devel
|\
i-w <-- "the stash"
(This is an ASCII-art depiction of [part of] the "commit graph": commit F
is the tip of the branch. Commit F
has, as its parent commit, commit E
: F
"points back to" E
. Meanwhile E
points back to its parent, and so on. The name devel
simply points to the tip-most commit, which is F
.)
If you did a regular commit, you'd get a new commit G
, pointing back to F
, and devel
would be changed to point to G
, "moving the tip forward".
The second difference with git stash
(vs git commit
) occurs at the "other end", as it were. When you run git stash apply
,2 what git does is much like the following. (There are many implementation details that make an accurate description tough, but I think this is the way to think about it. If you apply --keep-index
it's more complicated; keep in mind that this simplified picture is only "close enough" for the non---keep-index
case.)
To see how this applies to your situation(s), we have to look at what git pull
does, both with and without --rebase
.
The pull
command is best described as fetch
-then-merge
. (It even says so in the manual pages for it.) With --rebase
, it's best described as fetch
-then-rebase
. So we have two very different cases with the two different ways of invoking pull
.
The fetch
step is easy enough to describe. You fetch
from some "remote", which tells git to call up the remote repository over the Internet-phone :-) 3 and ask it what branches and commits and such it has. Then your git has their git hand over any new goodies, which your git stores away in your repository, so that you have everything they do, plus of course anything of your own.4
Again, let's say that you are on branch devel
and you run this kind of git fetch
step. Let's say further that when you do that, branch devel
looks like this:
... E - F - G <-- devel
When your git contacts their git, it may find new commits that you did not have before, such as commit H
, which points back to some earlier commit as its parent.
Perhaps H
's parent commit is commit G
. For this to happen, though, your colleague has to have had commit G
already. Thus, assuming you made G
, you need to have published (pushed) it, so that she got it, and made her H
based on G
.
But presumably you have not yet pushed G
, and her commit H
points back to F
. That's the more general case, so let's draw it:
... - E - F - G <-- devel
\
H <-- (her/their idea of what "devel" looks like)
Since commit G
is private to your repository, she, like everyone else, thinks the chain goes E - F - H
. You must now do something to incorporate "your commit" together with "her commit".
The way that's the most accurate description of the work that occurred, is for you to make a new merge commit M
:
... - E - F - G - M <-- devel
\ /
H
This is what git merge
will do, so it's what will happen with a plain git pull
.
The annoying thing with being completely accurate, history-wise, is that it gets you these "meaningless merges".5 So what you can do is, instead, copy your old commit G
to a new, slightly-different commit, G'
. There will be two changes between G
and G'
: (1) in the work-tree associated with G'
, you'll include her changes from H
first; and (2) in G'
, you'll say the parent-commit is H
rather than F
. This will look like this—let's move your old G
up to make the line go E - F - H
:
G [no longer needed, hence abandoned]
/
... - E - F - H - G' <-- devel
This is a "rebase" operation: copy your existing commits, changing their work-directory contents as needed, tacking the new commits onto the appropriate place (H
), and then making your branch-tip point to the last commit in the new copies.
This works even if you made a whole bunch of commits, G1
through G5
or whatever, it just takes more copying.
When you use git pull --rebase
, git does this for you. First it uses fetch
to bring over any new commits, and then—if there are some new commit(s)—it rebases your previous commits onto the new ones.6
So now we can get back to git stash
. If you have not made any new commits of your own on devel
, but have some work-in-progress, and you use git stash
to save it, you get this:
... - E - F <-- devel
|\
i-w
You now use git pull
without --rebase
, and it brings in commit H
("hers"—we're skipping over the letter G entirely, reserving it for now) and does a merge. Git does this as a "fast-forward merge" since you have no commits of your own, and you get this:
... - E - F - H <-- devel
|\
i-w
You then git stash apply
, which makes git look at the changes between commits F
and w
and merge them in to your working directory. That is, it applies your changes to the working directory for commit H
. Once you also drop
the stash (or if we just don't bother drawing it), git add
your changes, and git commit
, you get a new commit. For some reason :-) let's call it G'
instead of G
. So you now have:
... - E - F - H - G' <-- devel
which looks exactly the same as if you'd committed first, then run git pull --rebase
. In fact, the "abandoned" G
commit in the earlier case is really the same commit as the (dropped, i.e., abandoned) stash-bag commit!7
But, what if you did already make some commit (or several, but we'll just use one) commit, G
, before you git stash
some more changes? Then you have this:
... - E - F - G <-- devel
|\
i-w <-- stash
Now you git pull
(without --rebase
) and pick up her commit H
and merge it:
H
/ \
... - E - F - G - M <-- devel
|\
i-w <-- stash
Finally, you apply
the stash, make sure it's all good, drop
it, git add
, and make a new commit N
:
H
/ \
... - E - F - G - M - N <-- devel
and you have one of those annoying "meaningless merges". It came in when you did the git pull
without --rebase
.
The short version, then, is that git stash
only saves your bacon (avoids annoying merge) if you have no commits of your own. The git pull --rebase
method is more general. (Although, the problematic "upstream rebase" case notwithstanding, I much prefer doing a separate git fetch
step. Then I look over what came in, and choose whether to rebase or merge. But that's up to you.)
1Specifically, it makes at least two commits. First it makes one for the current index, i.e., what you'd get if you did git commit
without any git add
, git rm
, etc, and forcing the commit to exist (a la --allow-empty
) even if the tree is unchanged. Then it makes a multi-parent commit, i.e., a merge commit, with the current working directory as its contents. All these commits are done in a way that does not move the branch-tip. For additional details see this answer.
2I recommend using git stash apply
, checking the result, and then using git stash drop
if you're satisfied with the effect of apply
. The pop
command just means apply
-then-drop
, i.e., it assumes that you are satisfied. But if you use git stash
a lot you may have multiple stashes, and you might accidentally apply the wrong one, or too many of them, or something. If you're in the habit of "apply
first, get everything all set, and only then drop
", I think you're likely to make fewer mistakes. Of course, people differ. :-)
3Unless the "remote" is really local, e.g., file://whatever
, or a local path; or conceivably in the future there may be some non-"Internet" URLs. Git does not really care how it gets the new stuff from the remote, only that it can find out what the remote has, and bring that over so that it's now local.
4When you use git pull
, it invokes fetch
with some special limits turned on, so that you fetch only stuff that you intend to merge (or rebase-onto). In pre-1.8.4 versions of git this also inhibits updating your local "remote branch" entry, i.e., fetch fails to save a bit of useful information. As the release notes for git 1.8.4 put it:
this was an early design decision to keep the update of remote tracking branches predictable, but in practice it turns out that people find it more convenient to opportunistically update them whenever we have a chance, and we have been updating them when we run "git push" which already breaks the original "predictability" anyway.
5They do have some meaning (they mean you and she worked in parallel), but by next week if not earlier, nobody cares. That's just noise. This is generally true of all kinds of commits: if you try something, and then make a few more commits and have to back out the earlier "try something" as a total failure, the attempt plus the backing-out are probably just noise. If these commits are all private (non-published) you can use an interactive rebase to "edit history" to make it look like you never bothered doing the failed experiment. At the same time, the failed experiment might actually be useful information: "don't try it this way, that doesn't work". It's up to you to figure out what is Good Information, and what is Meaningless Noise.
6It's worth noting that git pull --rebase
is extra-clever in the case of an "upstream history rewrite". Suppose, before you pull, you have this:
...-o-x-x-Y <-- branch
`------- origin/branch
where o
and x
represent "their" commits and Y
is/are are "your" commits (they could be Y1-Y2-Y3
etc; it works out the same in the end). Suppose that when you run the git fetch
step, it turns out "they" rebased branch
themselves, so that instead of o-x-x
as what's "on" origin/branch
, you get o-*-*-*
:
...-o-x-x-Y <-- branch
\ `------- old origin/branch
*-*-* <-- FETCH_HEAD, to become new origin/branch
It's obvious (well, this drawing should make it seem obvious...) which commits were rebased upstream: they're the ones spelled *
instead of x
. So it's also obvious (heh) that git can rebase the Y
chain onto the tip *
commit, as pointed to by FETCH_HEAD
:
...-o-x-x-Y [abandoned]
\
*-*-*-Y' <-- tip
`------- new origin/tip
If you use a "regular" fetch, rather than the one in git pull --rebase
, this updates the remote branch, origin/tip
, which obscures the "fork point" that's so easily identified here, at least up until origin/tip
is moved to point to the new tip. Fortunately there is enough information in git's reflogs to reconstruct it, and in git 1.9/2.0, now that git fetch
always updates the remote branches, there is a way to ask git to find the fork-point later, so that you can recover from upstream rebases more easily.
7More precisely, it has the same tree as commit w
in the stash-bag.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With