简体   繁体   中英

Git stash apply - does git try to merge before applying?

Let's say I have a local repo and made some changes - some were added to the index while some were only tracked but left in the workspace without being added to index.

If I were to stash the changes, keep working (after stashing) on the same branch and then git-apply before adding or committing the changes that were made after the stash.

How would git process the "apply" ? is apply = apply + try to merge ?

What does git do internally when applying a stash into the repo ?

Should we always commit or stash our changes before applying other stashed changes into same branch ?

Thanks !

TL;DR

This is pretty long, so let me put the conclusions at the top, and then how I arrive at them.

Should we always commit or stash our changes before applying other stashed changes into same branch?

Unless you're pretty sure the stash will apply cleanly, I recommend doing so. Consider turning especially complicated stash situations into actual branches , using git stash branch .

Note that some versions of git stash apply and git stash pop are more careful than others. (Specifically, the constraint that "The working directory must match the index" is not true for every version of Git!)

Long

Let's say I have a local repo and made some changes - some were added to the index while some were only tracked but left in the workspace without being added to index.

The word track or tracked has some specific meanings in Git, neither of which seem to fit. In this case the appropriate definition of the word tracked means exists in the index . I am going to assume that all the files in your work-tree were "tracked" under this definition, and that you mean you did:

git check something
<edit several files>
git add <some of those files, but not all>

such that git status would have said, at this point:

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   wt-status.c

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   wt-status.h

(this is actual output from a git status command, minus the first three lines).

If I were to stash the changes,

It's important to know, at this point, what git stash does.

What git stash does is that it makes two, or if you ask it to, three, commits. These two (or three) commits represent a stash and are treated as a single unit for the purpose of stashes, and are referred to as a stash . The contents of the two commits—we'll ignore the optional third one and assume you did not use these forms of git stash —are:

  • a snapshot of the index, and
  • a snapshot of the work-tree.

(Because they are commits, they also have author and committer—which are of course you—and date and time stamps, and a log message, and so on. These are all auto-generated, except that you can specify the commit message as a stash message. Mostly they're not very interesting.)

Remember that the index contains the proposed next commit. 1 That is, the index is never 2 empty . It's just that right after git checkout master or whatever command you use to get started, the index usually matches the current commit . When it does match, git status says that there are no changes to be committed. That is, every copy of file in the index matches the copy of that same file in the HEAD commit.

Even if every file does match like this, one of the two commits that git stash makes will still have a copy of every file. 3 So the index commit is definitely not empty . It's just that its contents may match the HEAD commit's contents.


1 During a conflicted merge, the index expands, and no longer works as the proposed next commit. During this period, though, you can't run git stash either.

2 Well, hardly ever: you can have an empty index when you have no files at all, for instance.

3 Every commit that re-uses a file in some earlier commit, actually shares the file with that earlier commit. So if you make any new commit that exactly matches some existing commit's snapshot, the new commit takes almost zero disk space—it only needs space to hold the metadata. Again, the metadata is the part holding stuff like your name, the date-and-time, and your log message. Storing this will often take just one disk "block", whatever the block size is for your system. This may be as small as 512 bytes or 4KiB, depending on your minimum disk block size—though there are a lot of other factors that can affect this too.

Back to your question

If I were to stash the changes, [and then] keep working (after stashing) on the same branch ...

It's now important to know what git stash does immediately after making the two commits. The stash documentation calls these I (for index) and W (for work-tree); I like to call them the same, but in lowercase, and draw them like this:

...--F--G--H   <-- branch (HEAD)
           |\
           i-w   <-- refs/stash

Commits F , G , and H are the last three on the branch, with H being the tip of the branch. The name branch identifies commit H .

The i commit contains whatever was in your index , which means it contains all the files from the HEAD commit, overwritten by whatever files you git add ed. In my sample git status output above, I have all the files that make up the Git source, except that I overwrote wt-status.c with a new version. The w commit contains what you would get by running git add -u to that index. In this case, that would keep the updated wt-status.c that I already copied to the index (and is in my work-tree at this point), and overwrite the H -commit copy of wt-status.h with the copy I have in my work-tree.

Hence, after git stash , diffing the i commit against H would show that wt-status.c is different and everything else is the same. Diffing the w commit against H would show that both wt-status.c and wt-status.h are different.

The last act of git stash , at this point, is to run git reset --hard . That makes the index match commit H again, and makes my work-tree match commit H . All these files are tracked (they're all in the index) but all three copies match: the ones in H match the ones in the index, and the ones in the index match the ones in my work-tree.

... [and then] keep working (after stashing) on the same branch ...

So now, in your work-tree, you have some modified files. These files are still all tracked —there are copies of them in the (singular) index—but the work-tree versions are not copied into the index. The index matches commit H , but does not match the work-tree.

... and then git-apply before adding or committing the changes that were made after the stash.

git apply is a command, but it does not use the stash. git stash apply is a different command, though somewhat related. I think you mean: What if I were to run git stash apply now, without adding or committing?

Assuming you do mean git stash apply , we now have to get into more complications. The stash to be applied is the one to which refs/stash points. That's this iw pair of commits. 4 You can tell git stash apply to use both commits, but by default, it uses only the w commit . 5

In any case, assuming no third commit, git stash apply now moves on to the w commit. It runs git merge-recursive directly, bypassing all of the sanity checking that the top level git merge command uses (for good reason: we don't want to commit the result, for instance). This performs what I like to call the merge-as-a-verb action. Hence the answer to:

[does git stash apply ] try to merge?

is yes, it does .

Every merge, in Git, requires three inputs. Normally all three inputs are commits , 6 but in this case, one of them is your active work-tree. This is somewhat dangerous, because the merge-as-a-verb process writes on your work-tree . The front end git merge command always insists that you have a "clean" work-tree, so that scribbling on it does not destroy any in-progress work. Running git merge-recursive directly, as git stash apply does, bypasses this safety check. (Fortunately, there's a backup safety check, but it is hard to describe.)

In any case, this is now precisely what happens. The git merge-recursive that git stash apply runs is told to use the parent of the w commit as the merge base . In the example I drew, that parent commit is commit H . This commit's files go into index staging slot 1. It uses the current index content as the "ours" commit, ie, these files (from the current index's staging slot zero) go into index staging slot 2. Last, it uses the w commit itself as the "theirs" commit, ie, these files to into staging slot 3.

At this point, the really complicated merge rules kick in, because Git now has up to three index entries for each file. If all three match, or even if two match, the merge result is easy, and Git won't scribble on the work-tree copy. Only if all three are different will Git overwrite the work-tree file. In this last case, the backup safety check kicks in: if Git must overwrite the work-tree file, it must match the index copy. If not, git merge-recursive itself aborts and does nothing. 7 So, the fact that git stash apply does this complicated dance with your modified but never git-add -ed work-tree is probably safe.

This merge-as-a-verb process has the usual result: if Git can merge the changes from base version (the committed copy that is the parent of w ) to the w commit with the changes from the base to your current work-tree, you get a nice clean merge. If not, you get the merge conflicts written in your current work-tree.


4 Technically, the name stash points to the w commit. The git stash code finds the other commit, or other two commits depending on the type of stash, from the w commit.

5 If you have a three-stash commit, git stash apply insists on applying the third commit as well, no matter what you do. You can have git stash apply ignore the i commit, or use it, but if you have the third u commit, git stash apply will try to apply it. This can be quite annoying. We won't go into this here though.

6 This is significant because all commits are always completely read-only . Nothing Git does can overwrite these committed snapshots: they are safely recoverable forever, or at least for as long as the commit itself exists. Note, though, that git stash drop tosses the i and w stash commits into the trash, after which git gc may remove them.

7 There are a lot of corner and edge cases with rename detection here. It's possible that there are some missed cases. After more than ten years of Git usage, it's not all that likely , but if you do manage to run into one, the fact that it was unlikely is not going to help. :-)


Should we always commit or stash our changes before applying other stashed changes into same branch?

In theory, it should not be required. But because of all the complications shown above, I recommend doing so.

In general, I recommend avoiding git stash . It's useful for occasional quick work, but if it turns out that what you need to do is complicated after all, the quick stuff that git stash did may be inappropriate.

If you do have a stash for something you intended to be quick and easy, and it turns out to be complicated, there's a nice fix for this. First, commit or stash whatever you are in the middle of. (If you use git stash , remember that it might get complicated! 😀) Then, instead of git stash apply , use git stash branch .

The git stash branch command takes a branch name. It will create a new branch with this name. Remember how we drew the stash above, then consider this picture:

         L--M   <-- branch2
        /
       /           N--O   <-- branch3 (HEAD)
      /           /
...--F--G--H--J--K--P   <-- branch1
           |\
           i-w   <-- refs/stash

Here, someone (probably ourselves) made a stash, but then forgot about it and make more commits. There's no guarantee that commit w (as compared to its parent H ) can apply cleanly to any of commits M , O , or P . But instead of trying to deal with w as a stash , we can turn it into its own branch, using git stash branch :

git stash branch xyzzy

This finds the iw stash (through refs/stash ), locates commit H —which is easy since w points directly to H —and makes the new branch name go there. So now branch name xyzzy identifies commit H .

We (or git stash ) now check out commit H . Then we apply the index commit i and the work-tree commit w , using git stash apply --index . Since i and w were made from H , these all go smoothly. The git stash branch now drops the stash; everything is ready for us to run git commit to make a commit from the index if appropriate, then to run git add -u and commit again (or for the first time on the new branch). The result is two commits, if we commit the index first, or one commit, which we can draw like this:

         L--M   <-- branch2
        /
       /           N--O   <-- branch3
      /           /
...--F--G--H--J--K--P   <-- branch1
            \
             I   <-- xyzzy (HEAD)

Instead of a stash, which is hard to work with, we have an ordinary commit (or maybe two, but I only drew one). All our normal tools work with this commit (or these commits).

some were only tracked but left in the workspace without being added to index

You can't use git stash apply in this case, since

The working directory must match the index.

https://git-scm.com/docs/git-stash

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM