Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between `git stash show -p stash@{N}` and `git show stash@{N}`?

I thought they should be basically the same, but when I tried

$ git stash show -p stash@{N}

and

$ git show stash@{N}

the latter shows some additional commit information, but the actual diff was much, much shorter. (The former shows about a dozen files, but the latter only one.)

So, what exactly is the difference between the two and why are they different?

Can I also rely on things like git diff stash@{M} stash@{N} to be correct?

like image 272
musiphil Avatar asked Feb 12 '14 20:02

musiphil


1 Answers

Stash bags

The thing saved by git stash is what I have taken to calling a "stash bag". It consists of two1 separate commits: the "index" commit (the staging area), and the "work tree" commit. The work-tree commit is a funny kind of merge commit.

Let me draw this again here (see the referenced answer for a much longer version), just to illustrate it properly. Let's assume for simplicity that you have a small repo with just one branch and three commits on it, A through C. You're on the one branch and make a few changes and then run git stash save (or just plain git stash). This is what you get:

A - B - C     <-- HEAD=master
        |\
        i-w   <-- the "stash"

Now you might make (or switch to) another branch, but for illustration, let's just say you leave that stash there and make more "regular" commits on master:

A - B - C - D - E    <-- HEAD=master
        |\
        i-w   <-- stash

The point here is that the "stash-bag", the pair of index and work-tree commits, is still hung off the same commit as it was before. Commits cannot be changed, and this is true of stash-bag commits too.

But now you make a new stash by making some changes (while still on master) and running git stash save again.

What happens to the old stash-bag? The "reference name"2stash, now points to the new stash-bag. But the old stash-bag commits are still in there. They just now require a "reflog" style name, stash@{1}.3

Anyway, what you have now is this:

A - B - C - D - E     <-- HEAD=master
        |\      |\
        i-w     i-w   <-- stash
          .
           -------------- stash@{1}

(When you use git stash drop, the stash script simply manipulates the reflog for the stash ref to delete the ID of the dropped stash-bag. That's why all the "higher" ones get renumbered. The actual stash-bag itself is garbage collected on the next git gc.)

This next bit is a key to understanding what's going on.

Any time git needs you to name a specific commit, you can do it any of many different ways.

Each commit has a "true name" that is the big ugly SHA-1 hash you see, values like 676699a0e0cdfd97521f3524c763222f1c30a094. You can write that. It always means the same commit. Commits can never be changed, and that's a cryptographic hash of the entire contents of the commit, so if that particular commit exists at all, that value is always its name.

It's not a good name for people, though. So we have aliases: things like branch and tag names, and relative names like HEAD and HEAD~2, and reflog-style names like HEAD@{yesterday} or master@{1}. (There's a command, git rev-parse, that turns name strings like this into hash values. Try it: run git rev-parse HEAD, git rev-parse stash, and so on. Most things in git use either git rev-parse or its big brother that does a lot more, git rev-list, to turn names into the SHA-1 values.)

(For a complete description of how to name a revision, see gitrevisions. Git uses SHA-1s for more than just commits, too, but here let's just think about commits.)

Git stash show, git show, and git diff

OK, finally, we can get to your git show vs git stash show, and git diff and so on. Let's tackle git stash show first as that's the one you are supposed to use with stashes. Moreover, the git stash sub-commands will verify that the commit you name—or, if you name no commit, the one found via the stash reference—"looks like" a stash, i.e., is one of these funny merge commits.

If you run git stash show -p, git shows you a diff (-patch). But what exactly is it showing?

Go back to the diagram with the stash-bags. Each stash-bag is hung off a specific commit. Above, the "main" stash is now hanging from commit E, and the stash@{1} earlier stash is hanging from C.

What git stash show -p does is compare that stash's work-tree commit, the w, against the commit from which the stash hangs.4

You can of course do this yourself. Let's say you want to compare the w in stash, which hangs off commit E, against commit E, which can be named by the branch-name master. So you can run: git diff master stash. Here the name stash refers to the (current) stash commit w, and master refers to commit E, so this produces the exact same patch as git stash show -p stash. (And, if you want to compare the w in stash@{1} against commit C, you just need to run git diff such that you name those two commits. Of course it's easier to just git stash show -p stash@{1}.)5

What about plain git show? This is a little more complicated. git show is happy to show a commit, and you gave it a stash reference (either stash itself, or one of the reflog variants). That's a valid commit identifier, and it resolves to one of the w work-tree commits in one of the stash-bags. But git show acts differently when it sees a merge commit. As the documentation says:

It also presents the merge commit in a special format as produced by git diff-tree --cc.

So git show stash@{1} shows you a "combined diff", assuming that commit w is a normal merge of commits C and i, producing w. It's not a normal merge after all, although a combined diff may actually be useful, provided you know what you're looking at. Read the --cc documentation under git diff-tree to see how that works in detail, but I'll note that --cc implies -c which includes this bit:

... lists only files which were modified from all parents.

In the case of a stash, if you've git add-ed files before running git stash, so that the i-vs-w diff is empty, you won't see those files in the output here.

Last, if you git diff stash@{M} stash@{N}: this is just asking git diff to compare the different work-tree commits. How much meaning that has, depends on what you're comparing, which will generally depend upon where the stash-bags are attached.


1Two or three, really, but I'm going to draw it as two. You get two commits with git stash save (or a plain git stash, which means git stash save). You get three commits if you add the -u or -a options to save untracked or all files. This affects stash restoration, but not the output from the git stash show command.

2A "reference name" is just a name, rather like a branch or tag name. There are many possible forms of reference name. Branches and tags are just names with special purposes. "Remote branches" are another form of these references, and "stash" is a reference as well.

In fact, HEAD is just another reference, although it's a very special reference. I's so important that if you remove the HEAD file, git will decide that your repository is no longer a repository after all.

With some special-case exceptions—HEAD, ORIG_HEAD, MERGE_HEAD, and so on—references all start with the string refs/. Branches start with refs/heads/, tags start with refs/tags/, and "remote branches" start with refs/remotes/. In other words, references have a "name space", generally starting with refs/ and then getting another word underneath that to identify where they live.

The stash reference is spelled refs/stash (and stops there, there is no refs/stash/jimmy_kimmel or anything like that).

3In fact, this really does use the reflog. This means, among other things, that stashes other than the "main" one, refs/stash, will can expire. (Fortunately, as musiphil notes, the default since git 1.6.0 is that these don't expire; you must configure expiration times for them to make this happen—which is probably not what you want anyway.)

4The clever way it does this, using the suffix ^ notation, is spelled out in my other answer.

5What if you want to look at the index-commits in these stash bags? Ah, good question! :-) The stash script does not have a good answer. The easy way to see these is to use the ^2 suffix to name the second parent of each stash, which is the i commit. And, if you have a stash with a third commit containing untracked or all files, that's the third parent: commit w looks like a three-parent merge and stash^3 gets at the third one. But again, w is not a normal merge, so it's tricky. Probably the best easy way to look at all the parts of a stash is to turn it into its own separate branch, using git stash branch.

like image 97
torek Avatar answered Sep 20 '22 02:09

torek