简体   繁体   中英

How can I retrieve the content of a specific stashed file after have pull the new version of my project form GIT?

I am not so into GIT and I have the following problem.

I have some uncommitted file in my project that I have stashed by this statment:

git stash

Then I have pull the repository taking the work of a coolegue, that seems have correctly overwrite the changes of my stashed file.

Now I want to retrieve the content of a specific stashed file without lose the modification of my collegue.

I don't want to override the pulled version but only access to the code of the stashed version of a specific file.

Performing git stash list I obtain:

$ git stash list
stash@{0}: WIP on master: fd2a59b First version of iterate/aggregate for data received from dataservice
stash@{1}: WIP on master: 4910263 DSS project added

How can I do it from the shell?

Solved by myself:

1) First show the stashed file using the git stash show statment:

$ git stash show
 glis-toolkit/src/main/synapse-config/api/glisTest2.xml | 8 ++++++++
 1 file changed, 8 insertions(+)

2) Then I save this stashed version (from the stash 0) in another backup file by:

$ git show stash@{0}:glis-toolkit/src/main/synapse-config/api/glisTest2.xml  >  glisTest2Bck.xml

I don't know if it's possible to display the diff of a specific file into a stash easily.

However, to display the stashed code of stash@{0}

git stash show stash@{0} -p

If you're not satisfied with the output of the previous command, and since your working tree is clean, you can start by getting the wanted stash

# say you want to see diff of file my/file1.txt in stash{0}
git stash pop stash@{0}

# To Check your file by showing the diff between your work and your collegue's
git diff my/file1.txt

To go back to the first step

git stash

Hope that helps

There are several parts to this answer, because using git stash can be surprisingly difficult. The way you used it in this case is correct for your use-case but there are many traps here for the unwary and the new-to / not-so-into Git folks.

Stash makes several commits

The main thing to know, from which everything else flows, is this: git stash makes commits. In fact, it makes at least two commits; if you tell it to (with -u or -a ) it makes a third commit. All of these commits are on no branch, but they are still commits, and hence behave like commits. The format of these commits is that they look, to other Git commands, like a bizarre kind of merge commit , which means in general that you want to keep other Git commands from looking too closely at them, and mostly use git stash to deal with them.

The index and work-tree, and git checkout

What git stash commits are the index and the work-tree . The work-tree is the easiest to explain, by far: Git stores files in an internal, Git-only format, so to work with the files, you need a place where they are stored in the normal computer format. That's the work-tree. You can put, in this work-tree, files that are not going to be stored in Git as well, and this is where the index first comes in.

Git's index has several uses, but this is the main one: it is "where you build the next commit". That is, you start out, in any Git repository, with a current commit that you ran git checkout to get. (On the first git clone , Git runs git checkout master for you. Well, it's usually master anyway, but it's definitely something checked out.) This picks a branch name, uses it to find the tip commit of the branch, and makes that branch and that commit your current branch and commit. Then it fills the index—which was completely empty, the very first time—from that particular commit, so now the index has in it all the files that are in the current (or HEAD ) commit. When adding these files to the index, Git also copies them into the work-tree, and so now you have the HEAD commit equal to the index which is equal to the work-tree.

If you now git checkout some other branch/commit, Git changes HEAD to account for the new branch and tip commit, removes the old files (for the previous HEAD) from the index and work-tree, adds the new files to the index and work-tree, and once again you have the HEAD commit equal to the index which is equal to the work-tree.

Note that in all of this shuffling-about, any file that is in the work-tree that is not in the index, goes untouched. These are your untracked files, and that's what it means for a file to be untracked: A file is tracked if and only if it is in the index. (This only has a little to do with git stash itself, but it's crucial to using Git and .gitignore , because .gitignore has no effect on a tracked file.)

To make a new commit yourself, you first modify a file in the work-tree, then use git add to copy the new version into the index. This makes the new file ready to be committed, with the data it has right at the time you run git add . This leaves all the existing index files the way they are, so the new commit will still have all the other files unchanged. To add a file that isn't already in the index, you create it in the work-tree and again run git add . The copies the file into the index, just as before, but this time it's not overwriting an existing copy. To delete a file, you run git rm , which removes it from both the index and the work-tree.

This process of adding (overwriting existing or adding new) files to the index, and removing files from both index-and-work-tree, is what we mean when we say staging files for commit. This is why the index is also called "the staging area": it's where we copy the updated work-tree files into, to get them staged. Note that unless you git rm everything, the index itself is never actually empty: it's just that once you git commit the index, the index and the new commit you made, now match. (This makes the --allow-empty flag to git commit a bit misleading.)

Back to git stash , specifically the "save new stash" sub-command

Again, git stash save makes (at least) two commits: first, it commits the index itself—which is really easy since that's how Git always makes commits—and then it makes a second commit that consists, in effect, of git add ing all the tracked work-tree files, ie, copying them into the index, and then committing again. For internal reasons, though, it makes this second commit as a merge commit , merging HEAD and the index commit it just made. We don't have to care as long as we use git stash to deal with this weird merge. It's only if and when we step outside git stash that we suddenly have to care about it.

After git stash makes those commits, it mostly does the equivalent of git reset --hard HEAD , although here again there are flag options to change this. (For much—perhaps too much—more about this process, see How to recover from "git stash save --all"? ) What git reset --hard HEAD does is to re-set (hence git reset ) three things:

  1. It moves (ie, re-sets) the current branch from its current commit to the specified new commit. Because the specified new commit is HEAD , which is the current commit, this is like trying to drive from your kitchen to your kitchen: you may scurry around for a while, but you just end up exactly where you started.

  2. It re-sets the index. That is, it makes the index match the HEAD commit. This un-stages all your staged changes, un-removing git rm -ed files, un-adding any totally-new-files, and recovering from the HEAD commit any files you modified and then staged.

  3. It re-sets the work-tree. That is, it makes all tracked files in the work-tree match their versions in the HEAD commit and (now) the index.

As a kind of mental short cut, though, you can think of this as "ream out the index and work-tree". The stash code just saved them both, as commits, so it's now safe to clean them out. Now the index and work-tree both look like they would if you had just freshly run git checkout on the current commit. This is why you are now ready to use git merge or git rebase (whether or not you run it from git pull —remember, git pull is just git fetch followed by merge or rebase, in all of these cases).

Applying the stash

The process of applying (or "popping") a stash can be more complicated than the process of saving it, but in practice , it tends to be simple. Well, except when it goes wrong, anyway. You just get to a (usually clean) commit-and-index-and-work-tree state as before, and run:

git stash apply

What this does is run a couple of git diff commands, to find out:

  • What's the difference between the saved index, and the commit that was current when git stash save saved that index?
  • What's the difference between the saved work-tree, and the commit that was current when git stash save saved that work-tree?

Usually, for each file, there's at most one difference (either via the saved index, or via the saved work-tree): you either staged the file, so the difference shows up in the saved index, or you didn't, so any changes you made show up in the saved work-tree (if there are no changes there's no difference at all). Git then kind of smashes these all together, for each file, and puts all the changes into your current work-tree. Your current index goes un-affected, by default, although there are fancier ways to git stash apply .

Once you are satisfied that the stash has applied correctly, you can run git stash drop . This discards the two saved commits (and if git stash save "pushed" an earlier stash into stash@{1} , effectively "pops" the rest back down so that the earlier stash is now stash@{0} , or just stash ).

If you're sure you want to toss the stash after applying it, without first checking for correctness, you can use git stash pop . This literally just turns into git stash apply && git stash drop internally. (Of course, it passes the specific stash through, if you're using git stash pop stash@{n} .)

Some hard cases, like XML files

The process of applying each change—each git diff from commit-that-was-HEAD-at-the-time, to saved index or saved work-tree—uses Git's full merge powers. This is a simple(ish) mechanical process, but it works surprisingly well for most files. XML files, unfortunately, tend to defeat Git's not-exactly-super powers.

In this case, you may want to do exactly what you did here.

Because git stash save simply makes two commits (that are not on a branch), you can use all the usual Git tools to deal with these two commits. But once you do so, you must be careful, because the work-tree commit looks like a merge commit. (Well, in fact, it is a merge commit.) Fortunately, git show <commit>:<path> doesn't care if the commit is a merge: it just extracts the saved version of the file.

Each stash name or reflog name points to the saved work-tree commit. Hence:

git show stash:path/to/file.xml

prints the saved work-tree contents of that file. Redirecting that to a new file in your work-tree gives you a chance to examine the saved contents. So this is a fine thing to do.

Watch out, though, for git show stash . For those of us with dyslexia, 1 it's really easy to use this instead of git stash show . But git show <commit> does care if the <commit> is a merge commit. While git stash show knows to treat the work-tree commit as a separate, non-merge-y commit, git show —which sees stash as a commit ID, which then works but gets you the work-tree commit—tries to treat it as a merge commit. It tries to show a combined diff , which often just ends up showing nothing at all. (The most confusing case occurs when it does show something, but not all of what you stashed, which happens when you first staged a file, then modified it again in the work-tree.)

TL;DR summary (if it's not too late): git show stash:path/to/file is OK, but git show stash is always a mistake. Mostly you want git stash apply , but for odd cases, git show stash:path/to/file will get you the saved work-tree version. And, for really complicated cases, see my other longer answer and consider using git stash branch .


1 Dyslexics of the world untie!

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