peter-b peter-b - 1 year ago 63
Git Question

How do you stash only files that have been added?

For example, a

git status
gives the following:

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: app/src/[....]
modified: app/src/[....]
new file: app/src/[....]
deleted: app/src/[....]
modified: app/src/[....]
modified: test/unit/[....]
modified: test/unit/[....]
new file: test/unit/[....]
deleted: test/unit/[....]
modified: test/unit/[....]

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

modified: test/unit/[....]

Untracked files:
(use "git add <file>..." to include in what will be committed)

app/src/[....]/
app/src/[....]/
app/src/[....]/


(I have blanked the filenames)

How would I stash just the changes I have
git add
ed (i.e. "Changes to be committed", not unstaged changes or untracked files) so I can transfer them to another branch instead?

Answer Source

Git's basic storage mechanism is "the commit"—in fact, all git stash does is make a couple of somewhat unusual commits—so Nick Volynkin's answer is a correct one. It could perhaps use a bit of expansion though, and there are easier (well, potentially easier) methods.

I'm not a very big fan of git stash, but if you're used to using it, here's the simplest of the other methods:

  1. git stash save (aka git stash). This writes two commits, one based on the current index and a second to hold as-yet-unstaged work-tree files. (If you need to hold untracked files, you can add the -u flag, and then the stash script adds yet a third commit. Usually you can just leave these untracked files floating around in your work tree untracked, though.) These commits are not on any branch, they are only on/in the special "stash" ref. Meanwhile you're still on the "wrong" branch, which I'll call wrongbr below.

  2. git checkout the branch you want these on. Now you're on the right branch.

  3. git stash apply --index. This uses the special stash commits made in step 1, while also leaving them there (apply) in the stash. The --index is very important: it tells the stash script to keep the index and unstaged files separate, i.e., give you back the staged and unstaged setup you had earlier.

    If all goes well, you're now in a position to git commit the changes to the branch you want them on. The previously staged files are again staged, and the unstaged files are still unstaged, because you apply-ed the stash with --index. The commit will commit the staged files, leaving the unstaged files unstaged.

  4. Now you can get back on the other "wrong" branch (where you made the stash initially) and git stash apply or git stash pop with or without --index. You may need to clean out any unstaged files (and it's safe to do so, they are still in the stash): git reset --hard, followed by git checkout wrongbr, then git stash pop. Note that pop is just apply followed by drop: we didn't want to drop the stash in step 3 (the stash has the only copy of the original modified-but-unstaged files), which is why we used apply there, but now we (presumably) do want to drop the stash, so using pop is OK.

There's a big potential pitfall in step 3, though: it's possible the stash won't apply correctly. If so, you must use some other method. This is one reason I don't really like the stash system: it falls down in hard cases and leaves you needing to know the tools you can use if you don't use stash. In which case, you can just use those tools most the time ... and then use stash as a convenient short-cut for cases where you're sure it will work.


Background: a commit takes whatever is in the index now—git ls-files --cached will show the complete contents, while git status trims this down to the "interesting" ones and adds additional useful information—and makes a commit out of them, with all the necessary tree objects and so on. The new commit's parent commit is whatever the current commit was before.

You want your new commit made on another branch. One way to do this is to make it now, on the current branch; then copy that commit to a new, different commit on a different branch. To do the "copy commit to new, different commit on different branch" you can use git cherry-pick. It's true that you can use git rebase: under the covers, it uses git cherry-pick itself. But rebase is not quite the right tool: it's designed for en-masse cherry-picking, and you have just one commit; and at the end it uses git reset to move a branch label, but not quite in the way you want. You can make it work, but there are some more appropriate tools.

Let's go back to the original problem though: you want to take the current index and use it to make a new commit, but on another branch. This would be easiest if you could just switch to the other branch now, without doing anything else, and then make the new commit.

Chances are good that you can do this. Just git checkout otherbranch and then git commit. There are three possible cases here:

  1. The other branch doesn't exist yet. Great! Use git checkout -b newbranch to create it, starting from where you are now. Then git commit. You're done, unless you want to rebase the new branch to start from somewhere other than "where you are now". If so, use git rebase on the new branch. Note that you can do that rebase later, after you've taken care of the unstaged files.

  2. The other branch does exist, and—lucky you—git checkout otherbranch works fine. Do that and commit, and you're done. You can then git checkout whatever branch you want for the unstaged files.

  3. The most annoying case: the other branch does exist, but git checkout tells you that you'll overwrite something that you haven't committed.

Case 3 is the one where you need to commit-or-stash.

What to do here depends on what you are most comfortable with. You can, for instance, try the four-step stash method described above as the simplest alternative.

For myself, though, I would just commit now, on the "wrong" branch, then commit again (or use git stash) to get the unstaged files out of the way. This gives me a commit I can git cherry-pick into the right branch. Here's an example sequence that's likely to work:

  1. git commit to make the commit, but on the "wrong" branch (let's call your current branch wrongbr for reference below).
  2. git stash save to save the unstaged changes (or, with -u, untracked files too).
  3. git checkout the branch you want the commit to have been on, e.g., git checkout rightbr.
  4. git cherry-pick wrongbr. If this succeeds, good; if not, edit files as needed to clean up after the merge issues, then git commit the result.
  5. git checkout wrongbr: we will now fix this up by removing the commit copied in step 4.
  6. git reset --hard HEAD^: this drops the commit that we copied.
  7. git stash pop (or git stash apply && git stash drop which does the same thing, the apply variant simply giving you the opportunity to inspect the result before you drop the stash).

Note step 4 here: git cherry-pick takes the named commit (the tip of wrongbr, which contains the commit-we-want that's simply on the wrong branch), compares it with its parent, and then attempts to apply the resulting diff to the current branch. This may need to do a 3-way merge, if the files in the current branch differ a lot from their corresponding files in wrongbr. This is the same place the complications occur in the simple case of just checking out rightbr and committing initially. That is, we're doing this long version because the "most annoying" case occurred when we tried to just git checkout rightbr before committing, so there's a good chance we need to do some fixing-up. This is also likely to cause problems with the original 4-step stash method.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download