Search code examples
gitgit-branchgit-diffgit-stashgit-patch

`git stash pop` confusion after `git stash -k` and changing the branch


I had a situation I don't really understand with git 2.26.2:

Implementing a new feature in a feature branch (say n-feature), I discovered two minor issues that should be fixed in the main branch (say master) as the new feature would take some time to be completed. So I did this:

  1. git add --interactive and "patch" the files with the new feature's changes, leaving out the parts that should go into the main branch.
  2. "Stash the rest" using git stash -k.
  3. git commit, committing the changes to n-feature.
  4. Checkout the main branch (git checkout master) and do git stash pop.

Doing a git diff I also saw the changes already committed to n-feature (and thus should not go to the main branch with the desired commit), so I recovered using these steps:

  1. git stash again on the main branch (as the stash had been "dropped" again when "popping" it)
  2. Checkout n-feature again
  3. git stash pop to apply the stashed changes found at the main branch

At that point, a git diff showed the right amount of changes, so I continued using:

  1. git stash to save the changes on n-feature again (I thought initial step #2 would have done the same thing)
  2. git checkout master again
  3. git stash pop to apply the new stash
  4. git diff - now the set of changes looked correct, so I continued:
  5. git add -u and git commit
  6. git checkout n-feature

(I might rebase n-feature on master, but the fixes are not essential for n-feature)

So I wonder:

  1. Why wasn't the set of changes correct of the first attempt?
  2. Is there an easier (shorter) way to do the same thing I eventually did (when discovering that some bug has to be fixed on the main branch)?

Solution

  • Performing git stash -k adds all the unstaged changes to the stash, and leaves the staged changes intact.

    This option is only valid for push and save commands. All changes already added to the index are left intact.

    In your case, since you've split the modifications on one or more files into different hunks, your file(s) appear to be both staged and unstaged. Therefore, if you perform git stash -k -- <my_files>, you're stashing the entirety of changes on those files, regardless if you've previously staged them as hunks or not.

    If you want to stage some changes and stash the remaining hunks, you need to use the patch option and enter git stash push -p (the -k option is not necessary, as -p implies -k). This version of the command will prompt an interactive menu, like for patch addition (git add -p), which will ask you what you would like to do: split the hunk of changes, stash it, not stash it, and so on.

    This option is only valid for push and save commands. Interactively select hunks from the diff between HEAD and the working tree to be stashed. The stash entry is constructed such that its index state is the same as the index state of your repository, and its worktree contains only the changes you selected interactively. The selected changes are then rolled back from your worktree.

    The --patch option implies --keep-index. You can use --no-keep-index to override this.

    In your case, just repeat the hunk splitting process that you've done during the staging, but this time select the hunks that you want to stash.


    Example to answer the question in the comments

    Let's say we're on the master branch with the file MyInterface.java:

    public interface MyInterface {
    
        void myCoolMethod();
    }
    

    Now, we create a new branch dev and switch to it. While on dev, we add a couple of new methods.

    public interface MyInterface {
    
        void myOtherCoolMethod1();
        void myCoolMethod();
        void myOtherCoolMethod2();
    }
    

    At this point, a git diff will show us that the lines with myOtherCoolMethod1 and myOtherCoolMethod2 have been added. But, we only want to commit on dev the line with myOtherCoolMethod1, while on master the line with myOtherCoolMethod2. So, we need to split the hunk of changes with a git add -p -- MyMethod.java, and stage only the first one.

    git add -p -- MyInterface.java
    
    # Here, Git shows us the changes on the file, 
    # so that we can decide how to split them in patches.
    
    diff --git a/MyInterface.java b/MyInterface.java
    index 1f42664..0c212de 100644
    --- a/MyInterface.java
    +++ b/MyInterface.java
    @@ -1,4 +1,6 @@
     public interface MyInterface {
    
    +       void myOtherCoolMethod1();
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file
    
    
    # checking the possible options by entering ?
    
    (1/1) Stage this hunk [y,n,q,a,d,s,e,?]? ?
    y - stage this hunk
    n - do not stage this hunk
    q - quit; do not stage this hunk or any of the remaining ones
    a - stage this hunk and all later hunks in the file
    d - do not stage this hunk or any of the later hunks in the file
    s - split the current hunk into smaller hunks
    e - manually edit the current hunk
    ? - print help
    @@ -1,4 +1,6 @@
     public interface MyInterface {
    
    +       void myOtherCoolMethod1();
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file
    
    
    # splitting the change on the file in two hunks
    
    (1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
    Split into 2 hunks.
    
    
    # staging the first hunk
    
    @@ -1,3 +1,4 @@
     public interface MyInterface {
    
    +       void myOtherCoolMethod1();
            void myCoolMethod();
    (1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
    
    
    # skipping the second hunk
    
    @@ -3,2 +4,3 @@
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file
    (2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n
    

    As you can see, entering the s option in the interactive menu allows us to split the current hunk of changes. Of course, in more complex cases, the s can be entered multiple times to further split the current change into the number of patches we need.

    At this point, if we perform a git status, we can see that the file looks both staged and unstaged. This is because the file has only been partially staged.

    git status
    On branch dev
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
            modified:   MyInterface.java
    
    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:   MyInterface.java
    

    Now, to simulate your case, we need to stash the remaining changes, but this time we won't perform a git stash -k, but a git stash -p which will prompt the exact interactive menu of git add -p.

    # stashing hunks of changes
    git stash push -p
    
    diff --git a/MyInterface.java b/MyInterface.java
    index 1f42664..0c212de 100644
    --- a/MyInterface.java
    +++ b/MyInterface.java
    @@ -1,4 +1,6 @@
     public interface MyInterface {
    
    +       void myOtherCoolMethod1();
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file
    
    
    # splitting again the change into two hunks
    
    (1/1) Stash this hunk [y,n,q,a,d,s,e,?]? s
    Split into 2 hunks.
    
    
    # skipping the first hunk since we've staged it
    
    @@ -1,3 +1,4 @@
     public interface MyInterface {
    
    +       void myOtherCoolMethod1();
            void myCoolMethod();
    (1/2) Stash this hunk [y,n,q,a,d,j,J,g,/,e,?]? n
    
    
    # stashing the second hunk that we didn't stage
    
    @@ -3,2 +4,3 @@
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file
    (2/2) Stash this hunk [y,n,q,a,d,K,g,/,e,?]? y
    

    At this point, we can record a commit on dev with just the myOtherCoolMethod1 change, checkout master, and pop on the master branch. After that, if we perform a git diff, we will see that only the change myOtherCoolMethod2 has been brought to the master branch, unlike how it was happening to you where the entirety of changes was stashed.

    # recording the partial change on dev
    git commit -m "add myOtherCoolMethod1"
    
    # switching to the master branch
    git checkout master
    
    # popping the partial change with myOtherCoolMethod2 on master
    git stash pop
    
    # making sure that only the partial stash was applied
    git diff
    diff --git a/MyInterface.java b/MyInterface.java
    index 1f42664..d3169e9 100644
    --- a/MyInterface.java
    +++ b/MyInterface.java
    @@ -1,4 +1,5 @@
     public interface MyInterface {
    
            void myCoolMethod();
    +       void myOtherCoolMethod2();
     }
    \ No newline at end of file