简体   繁体   中英

Does “git stash” internally “commit” to my local repo?

I only ever manually commit to my local repo just before push ing to the remote repo.

But more often I pull to get my coding partner's changes.

Sometimes we've both worked on the same file and there's a conflict. In these cases he's told me to do a git stash before my git pull and then a git stash pop afterward.

But sometimes this results in git telling me next time that I can't pull because I have unmerged files. These are usually experimental changes in my local tree that I don't wish to commit or push .

A couple of times I've needed to send my work in and the result has been intermediate revisions in the remote repo including my local experiments, debug code, etc, that I never wished to send. I want to avoid making such mess.

Is this due to stash modifying my local repo? If so, how can I avoid that? If not, what else could be causing it? I'm a total noob at git and only use these few commands.

I want to first mention here that the term index means the same thing as staging area , and that you should remember that there are three versions of a file "active" at any one time: the HEAD version, the index or "staged" version, and the work-tree version. When you first run git checkout <branch> you generally have all three versions matching up. Any committed version is permanent—well, as permanent as the commit—and unchangeable: you can't touch the one stored in the current commit. You can overwrite the index and work-tree versions at any time, but the normal pattern is:

  1. Check out the committed version: copy commit to index, then index to work-tree.
  2. Work on the work-tree version.
  3. Use git add to copy the work-tree version back to the index.

Repeat steps 2 and 3 until satisfied; or use git add --patch to build up an index version that's kind of like the work-tree version, but different. (Typically one does this to make a committable version of some file that doesn't have extra debug stuff in it, while running the debug one.) This does mean that the index and work-tree can differ from each other and from the HEAD commit.

If and when you do run git commit , this makes a commit from whatever's in the index / staging-area right then . This is why you have to keep git add ing all the time, to copy from work-tree into index.


As Sajib Khan answered , git stash save does make commits. More precisely, if git stash save does anything (sometimes it does nothing, if there are no changes), it makes at least two commits. If you use the --untracked or --all flag, it makes three commits. The git stash documentation has a small diagram of this under its DISCUSSION section. Like the documentation, we'll mostly ignore the third commit. 1

What's unusual about these commits is that they are on no branch. The special reference name refs/stash points to the newly created w (work-tree) commit. It has at least two parents, one being the HEAD commit and the other being the i (index) commit. With --untracked or --all there is a third parent (which I call u ) holding the extra, untracked, files.

In all but one case 2 that we'll also ignore here, after saving away the index and work-tree versions of each file in the i and w commits, git stash save then runs git reset --hard HEAD to replace the index and work-tree versions of those files with the versions stored in the HEAD commit. So your work is now saved, and can be restored later, but no longer exists in either index (aka staging area) or work-tree.


1 If (and only if) you use the --all or --untracked option to create the third commit, Git also runs git clean with appropriate options to delete the files stored in this third parent. Keep this in mind: any existing untracked files (whether ignored or not) are never included in either i or w . They are not saved at all, and hence not cleaned away either, unless you use these extra options. Note that the definition of an untracked file is simply any file that is not in the index right now . The last two words are critical as well, in a case you are not yet running into, but could eventually.

2 The one case occurs when you use the --keep-index option. In this case, the git stash save code does something fairly tricky: after making the i and w commits, instead of resetting the index and work-tree to HEAD , it resets them to what's in the i commit. The purpose of this is to arrange the work-tree to hold the proposed new commit, so that programs that test work-tree files can test the "to be committed" versions of the files. There are several traps here for the unwary, however: see How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?


Where you're going wrong

Once you have a stash—ie, i and w commits—saved away, you can mostly-safely run git pull , 3 or better, git fetch . This will obtain new commits from the other Git you have your Git remembering as origin , remembering them via origin/master and origin/develop and origin/feature/tall and so on. Then the second step of pull , which is rebase or merge , will rebase—ie, copy—your existing commits if you have any, or merge your existing commits if you have any, on/with the latest commits you have brought in, and adjust your own current branch to point to the result.

So far, everything has gone well and is just what you are doing. But now we get to the tricky part.

Now you run git stash pop as your co-worker / coding partner suggests. I recommend starting with git stash apply instead of git stash pop , but when it fails—and it is failing, given what else you've mentioned—that doesn't really matter. 4 Either way Git attempts to apply the saved commits, so that the changes you saved in the index and/or the work-tree are restored. But this is, as I just said, tricky, because commits are snapshots, not changes . (Also, by default, git stash apply / git stash pop throws away the i commit. With --index , it tries to restore the i commit into the index, too.)

In order to restore the w commit as changes , rather than as a snapshot, Git uses its merge machinery. The stash script has this actual line in it:

git merge-recursive $b_tree -- $c_tree $w_tree

The effect is as if you had run a git merge —or even more close, git cherry-pick —command. Git compares your stashed work tree $w_tree (commit w ) to the commit that was HEAD ( $b_tree ) to see "what you changed", and compares your current index as turned into a partial commit ( $c_tree ) against that same $b_tree to see "what they changed", and merges them.

This merge, like any merge, can fail with merge conflicts. This is what you have described:

... can't pull because I have unmerged files ...

When a merge fails, it leaves the partially-merged results in the work-tree and the original sets of files in the index. Suppose, for instance, that file foo.txt has a merge conflict. Now instead of three versions of foo.txtHEAD (current commit), index, and work-tree—you have five versions! These are:

  • HEAD , as always;
  • index stage 1, the merge base version: this is the one taken from $b_tree , which is the tree that goes with whichever commit was HEAD back when you ran git stash save ;
  • index stage 2 or --ours : this is whatever was in the index when you started the git stash apply / git stash pop that failed. (This probably matches the HEAD version.)
  • index stage 3 or --theirs : this is whatever was in $w_tree , ie, your stashed changes; and
  • the version left in the work-tree, with the merge conflict markers.

Note that just as with git rebase and git cherry-pick , the ours/theirs git checkout flags are kind of reversed here.

Once you are in this annoying "unmerged index entries" state, there is very little you can do other than either finish it, or abort the operation entirely.

In this particular case, the git stash apply has already stopped partway through applying, and hence already aborted the subsequent git stash drop . You therefore still have your stash and can run git reset --hard HEAD to abort the attempt to apply the stash. Or, you can edit the work-tree files to finish the merge that Git was unable to do, and git add the files to copy them into the index, so that the index has the (single) merged entry takem from the work-tree, replacing the three higher-staged entries.

This is the same process you must do for any failed merge: you either abort it, and then figure out what to do later; or you finish it now.

Note that in general you should not just git add the work-tree files "as is", complete with conflict markers. While this solves the "cannot X, you have unmerged index entries", it leaves you with files full of conflict markers, which are not really useful.

These kinds of merge failures (merge conflicts) can also happen when you run git pull , if Git needs to merge some of your commits with someone else's commits. Or, Git may succeed at doing the merge on its own (or think it succeeds, at least), and then make a new merge commit . In this case you will be prompted to enter a commit message:

I've noticed that a side effect of the pull after the stash sometimes opens up vi for me to enter a commit message.

This indicates that you have made normal commits, on your branch, and that your git pull has run git merge , which believes it made a successful merge and now needs the commit message for this new merge commit.

You may want to use git rebase rather than git merge here. This is easier if you use git fetch followed by the second Git command, instead of using git pull .

Note that if you do use git rebase (and learn it, especially the very handy git rebase -i ), you can feel free to make all kinds of temporary commits, including:

... local experiments, debug code, etc ...

When you rebase, you'll copy those commits atop the other guy's commits, keeping your work as your own; and you can eventually use git rebase -i to "squash away" the temporary commits in favor of one big final "real" commit. You do have to be careful not to accidentally git push them (once you do, copies wind up everywhere and it's overly difficult to get everyone else to give them up).


3 I recommend not using git pull here: instead, split it up into its component parts. First, run git fetch to obtain new commits from another Git. Once you have the commits, you can look them over if you like. Then use git rebase or git merge to incorporate those commits into your branch(es). You need one git rebase or git merge per branch, but only one git fetch before all of them.

Once you're very familiar with how the two component parts it will do for you work, you can safely run git pull , knowing that when something goes wrong—and something eventually will—you'll recognize what happened and which step failed, and will know what to look at to figure out how to fix it.

4 For completeness, though, note that git stash pop just means git stash apply && git stash drop . That is, attempt to apply the stash; then if Git thinks that went well, immediately drop the stash. But sometimes Git thinks it went fine, when it didn't. In this particular case it's nice to still have the stash handy, and git stash drop makes getting it back very hard.


Stash actually commit/save your local changes in a temporary box, not in your working tree .

$ git stash

You can see the list of stashes -

$ git stash --list

Before Pulling the changes just make sure you stashed all unnecessary/experimental changes perfectly.

$ git stash save 'provide stash message'       # better give a stash message
$ git stash     # see if all the changes are stashed

You can also apply the stash instead of pop (if don't want to delete it). You can also delete a stash without applying it by git stash drop (delete the #1 stash). Basically, pop = apply + drop

$ git stash apply stash@{0}     # get back the last (#1) stash changes
$ git stash apply stash@{1}     # get back the #2 stash changes

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