diff mbox series

[RFC,v1] stash: implement '--staged' option for 'push' and 'save'

Message ID 87lf2zz59w.fsf@osv.gnss.ru (mailing list archive)
State Superseded
Headers show
Series [RFC,v1] stash: implement '--staged' option for 'push' and 'save' | expand

Commit Message

Sergey Organov Oct. 11, 2021, 8:16 p.m. UTC
Stash only the changes that are staged.

This mode allows to easily stash-out for later reuse some changes
unrelated to the current work in progress.

Unlike 'stash push --patch', --staged supports using of any tool to
select the changes to stash-out, including, but not limited to 'git
add --interactive'.

Signed-off-by: Sergey Organov <sorganov@gmail.com>
---

This operation that is essentially just a 'git commit', but to the stash
rather than to the current branch, is somehow missed, complicating the
task that is otherwise simple and natural. For example, see discussions
here:

https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible

Changes in v1:

  * Implement separate stash_staged() instead of re-using and changing
    stash_patch()

  * Add test

  * Minor documentation cleanup

 Documentation/git-stash.txt | 34 ++++++++++++++--
 builtin/stash.c             | 80 ++++++++++++++++++++++++++++++++-----
 t/t3903-stash.sh            | 11 +++++
 3 files changed, 113 insertions(+), 12 deletions(-)

Comments

Eric Sunshine Oct. 11, 2021, 9:21 p.m. UTC | #1
/On Mon, Oct 11, 2021 at 4:17 PM Sergey Organov <sorganov@gmail.com> wrote:
> Stash only the changes that are staged.
>
> This mode allows to easily stash-out for later reuse some changes
> unrelated to the current work in progress.
>
> Unlike 'stash push --patch', --staged supports using of any tool to
> select the changes to stash-out, including, but not limited to 'git
> add --interactive'.

s/using of any/use of any/
...or...
s/using of any/using any/

> Signed-off-by: Sergey Organov <sorganov@gmail.com>
> ---
> diff --git a/builtin/stash.c b/builtin/stash.c
> @@ -1656,6 +1716,8 @@ static int save_stash(int argc, const char **argv, const char *prefix)
> +               OPT_BOOL('S', "staged", &only_staged,
> +                        N_("stash in patch mode")),
>                 OPT_BOOL('p', "patch", &patch_mode,
>                          N_("stash in patch mode")),
>                 OPT__QUIET(&quiet, N_("quiet mode")),

Copy/paste error in new help/description string?
Sergey Organov Oct. 11, 2021, 9:55 p.m. UTC | #2
Eric Sunshine <sunshine@sunshineco.com> writes:

> /On Mon, Oct 11, 2021 at 4:17 PM Sergey Organov <sorganov@gmail.com> wrote:
>> Stash only the changes that are staged.
>>
>> This mode allows to easily stash-out for later reuse some changes
>> unrelated to the current work in progress.
>>
>> Unlike 'stash push --patch', --staged supports using of any tool to
>> select the changes to stash-out, including, but not limited to 'git
>> add --interactive'.
>
> s/using of any/use of any/
> ...or...
> s/using of any/using any/

Will fix, thanks!

>
>> Signed-off-by: Sergey Organov <sorganov@gmail.com>
>> ---
>> diff --git a/builtin/stash.c b/builtin/stash.c
>> @@ -1656,6 +1716,8 @@ static int save_stash(int argc, const char **argv, const char *prefix)
>> +               OPT_BOOL('S', "staged", &only_staged,
>> +                        N_("stash in patch mode")),
>>                 OPT_BOOL('p', "patch", &patch_mode,
>>                          N_("stash in patch mode")),
>>                 OPT__QUIET(&quiet, N_("quiet mode")),
>
> Copy/paste error in new help/description string?

Yep.

Thanks,

-- Sergey Organov
Ævar Arnfjörð Bjarmason Oct. 12, 2021, 9:18 a.m. UTC | #3
On Tue, Oct 12 2021, Sergey Organov wrote:

> Eric Sunshine <sunshine@sunshineco.com> writes:
>
>> /On Mon, Oct 11, 2021 at 4:17 PM Sergey Organov <sorganov@gmail.com> wrote:
>>> Stash only the changes that are staged.
>>>
>>> This mode allows to easily stash-out for later reuse some changes
>>> unrelated to the current work in progress.
>>>
>>> Unlike 'stash push --patch', --staged supports using of any tool to
>>> select the changes to stash-out, including, but not limited to 'git
>>> add --interactive'.
>>
>> s/using of any/use of any/
>> ...or...
>> s/using of any/using any/
>
> Will fix, thanks!
>
>>
>>> Signed-off-by: Sergey Organov <sorganov@gmail.com>
>>> ---
>>> diff --git a/builtin/stash.c b/builtin/stash.c
>>> @@ -1656,6 +1716,8 @@ static int save_stash(int argc, const char **argv, const char *prefix)
>>> +               OPT_BOOL('S', "staged", &only_staged,
>>> +                        N_("stash in patch mode")),
>>>                 OPT_BOOL('p', "patch", &patch_mode,
>>>                          N_("stash in patch mode")),
>>>                 OPT__QUIET(&quiet, N_("quiet mode")),
>>
>> Copy/paste error in new help/description string?
>
> Yep.
>
> Thanks,
>

I very much like this option, I've sometimes missed it in "git stash",
and was always going to dig into if there was some way to do it.

The one thing I'm a bit iffy on is if this is consistent with the
--staged options in other commands (with some taking --cached and/or
--staged), I think so, and this is a good name.

But is the -S option used as a shorthand for --staged somewhere else?
*Checks*, ah yes, for "git restore", then we use "stage" for
checkout-index/ls-files, the latter of which has a 's' (not
capital-letter 'S') shorthand.

I *think* that just -s/--stage would make more sense here, but I've only
looked at it briefly, but getting options consistent if possible is in
general quite nice for users, so we should think about it...
Sergey Organov Oct. 12, 2021, 11:20 a.m. UTC | #4
Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Tue, Oct 12 2021, Sergey Organov wrote:
>
>> Eric Sunshine <sunshine@sunshineco.com> writes:
>>
>>> /On Mon, Oct 11, 2021 at 4:17 PM Sergey Organov <sorganov@gmail.com> wrote:
>>>> Stash only the changes that are staged.
>>>>
>>>> This mode allows to easily stash-out for later reuse some changes
>>>> unrelated to the current work in progress.
>>>>
>>>> Unlike 'stash push --patch', --staged supports using of any tool to
>>>> select the changes to stash-out, including, but not limited to 'git
>>>> add --interactive'.
>>>
>>> s/using of any/use of any/
>>> ...or...
>>> s/using of any/using any/
>>
>> Will fix, thanks!
>>
>>>
>>>> Signed-off-by: Sergey Organov <sorganov@gmail.com>
>>>> ---
>>>> diff --git a/builtin/stash.c b/builtin/stash.c
>>>> @@ -1656,6 +1716,8 @@ static int save_stash(int argc, const char **argv, const char *prefix)
>>>> +               OPT_BOOL('S', "staged", &only_staged,
>>>> +                        N_("stash in patch mode")),
>>>>                 OPT_BOOL('p', "patch", &patch_mode,
>>>>                          N_("stash in patch mode")),
>>>>                 OPT__QUIET(&quiet, N_("quiet mode")),
>>>
>>> Copy/paste error in new help/description string?
>>
>> Yep.
>>
>> Thanks,
>>
>
> I very much like this option, I've sometimes missed it in "git stash",
> and was always going to dig into if there was some way to do it.
>
> The one thing I'm a bit iffy on is if this is consistent with the
> --staged options in other commands (with some taking --cached and/or
> --staged), I think so, and this is a good name.
>
> But is the -S option used as a shorthand for --staged somewhere else?
> *Checks*, ah yes, for "git restore", then we use "stage" for
> checkout-index/ls-files, the latter of which has a 's' (not
> capital-letter 'S') shorthand.
>
> I *think* that just -s/--stage would make more sense here, but I've only
> looked at it briefly, but getting options consistent if possible is in
> general quite nice for users, so we should think about it...

I grepped the Documentation/ and found -S,--staged in the
"git-restore.txt":

-S::
--staged::
        Specify the restore location.

, so I used the same shorthand, even though I personally would use -s as
well.

In addition, the problem with -s is that it's used in a lot of places
for entirely unrelated option(s).

Thanks,
-- Sergey Organov
Junio C Hamano Oct. 12, 2021, 12:04 p.m. UTC | #5
Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> The one thing I'm a bit iffy on is if this is consistent with the
> --staged options in other commands (with some taking --cached and/or
> --staged), I think so, and this is a good name.

We clearly define contrasts between "--cached" and "--index", but
the "--staged", which is a confusing synonym for nothing, does not
get in the contrast between the two, so I do not think you need to
worry about "which one is it?" in this case.

If something works only on the contents in the index, then it should
use "--cached".  If it works both on the index and the working tree,
then it should use "--index".  If you call it "--staged", it is
whatever it means ;-)

More importantly...

Whenever I think about a new "feature", I try to come up with a
story in which the feature effectively improves the end-user's life,
how it fits in the larger picture, and enables something that is
hard to do by combining other tools.

The kind of "story" I would aim for is like this.  Suppose we were
selling not "git stash -S" but "git stash -k".  The story would go
like this:

  Imagine that the change you have been working on started to take
  shape, and you estimate it would be a three-patch series in the
  end.  You also guess that so far you have enough to finish the
  first step fully, perhaps 40% of the second step and a little bit
  of the third step, all mixed together.

  You started to sifting the changes into the first step material
  and the rest, by using "add -p" etc., and you are reasonably sure
  that what you have in the index is in a good shape for the first
  commit.  But it is not easy to be sure, because what you can test
  is only in the working tree, so a mistake like having all the code
  already added in the index but forgetting to add to the index a
  declaration for a variable the code uses that is in the working
  tree is easy to make.  With "git stash -k", you can materialize
  only what is in the index to the working tree, while stashing away
  the changes in the working tree that haven't been added yet.  By
  checking the resulting working tree, you can be sure.

  - If the resulting working tree after "git stash -k" tests out
    OK, then you can make a commit and the "git stash pop" will
    give you the material for the second and the third step.
    You'd work to produce the remaining 60% of the second step and
    do "git stash -k" dance again before recording it.

  - It is possible that the resulting working tree does not work
    OK.  You may find that you forgot to "add -p" the declaration
    of a variable you used in the code that you already "add"ed.
    After "stash -k", the former is stashed away while the latter
    appears in the working tree, and the compiler complains.  In
    such a case, you can "git commit" the slightly broken state,
    "git stash pop" to recover the missed declaration, together
    with the material for the second and third step, into the
    working tree, use "add -p" and "stash -k" to prepare and
    verify the "fixup" commit for the first step.  Later you can
    "rebase -i" the first step into shape.

I unfortunately am coming up empty for "git stash -S".  And I do not
see a beginning of a good story at the stackoverflow entry you had
the URL for, either.  If we had one to support this feature, that
would help very much.

While I failed to come up with a good story for this new feature, I
however did come up with possible confusion and frustration that
end-users may feel while trying to use it:

 - I thought the result of "git add -p" was good, so I did "git
   stash -S", then after working further on, did another "git stash
   -S" a few times.  Now I have a handful of stash entries, but
   because all I can do is "git stash pop" them one by one,

   - I need to make commits for real, and 

   - because I wasn't given a chance to, these stash entries do not
     record material to write good log messages and I forgot why I
     did some of the changes in the way I did so.

   - My "add -p" seem to have missed some stuff that should have
     been added, but it is too late to correct, especially given
     that these stash entries cannot be "rebase -i"ed or "commit
     --amend"ed.

 - Also, how would I reorder these steps?  If I made real commits,
   instead of "stash -S", I am familiar with "rebase -i" to reorder,
   combine or split them, but because these are not real commits, I
   cannot use "rebase -i".

 - After making these "stash -S" entries, I popped a wrong one.  If
   I recorded them as real commits on a temporary work branch, its
   reflog would have helped me to recover from such a mistake, but
   because stash does not mix well with reflog, I am lost.

And I do not want to see us respond to these future end-user gripes
with "don't worry, we'll extend 'git stash save [-S]' with the '-e'
option to let you describe the change in detail, and enhance 'git
stash pop' with the '--commit' option to directly create a commit
using the message you wrote when you created the stash", or "don't
worry, we'll enhance 'rebase -i' to be capable of working on series
of stash entries".  These all look like complexity that only became
necessary because we added "git stash -S"---if the user committed
incrementally on a temporary work branch, none of the complication
would have been needed.

I think it is very possible that an answer to the above possible
end-user gripes is "no, this feature is not about sifting a big and
mixed changes in the working tree into multiple steps recorded in
the stash entries (instead of a series of commits on a branch), so
all the above end-user gripes are the result of using the tool for a
wrong job", and that is why I wanted to come up with a story in
which this feature effectively improves the end-user experience.
IOW, with the "frustration" story in the previous paragraph, I might
have been trying to drive screws into wood with this new feature,
which is a hammer and not a screwdriver.  If that is the case, then
I would want to see a story where the hammer is used to drive nails
instead, and nails do something good that screws don't.

I have a suspicion that this _might_ be coming from a hesitancy to
commit (e.g. a draconian commit hook that always pushes things out
immediately a commit is made), and somehow creating stashes is used
as a way to sidestep the real source of the problem (e.g. in Git,
commits on temporary branches are designed to be useful lightweight
tools to help advance the history recorded in the real branch, but
misguided hooks and policies prevent your branches to be used as
such).  I dunno.
Junio C Hamano Oct. 12, 2021, 12:34 p.m. UTC | #6
Junio C Hamano <gitster@pobox.com> writes:

> More importantly...
>
> Whenever I think about a new "feature", I try to come up with a
> story in which the feature effectively improves the end-user's life,
> how it fits in the larger picture, and enables something that is
> hard to do by combining other tools.
>
> The kind of "story" I would aim for is like this.  Suppose we were
> selling not "git stash -S" but "git stash -k". ...

To answer my previous "question", I guess this is usable in the same
scenario where "git stash -k" is useful.  After creating a bunch of
stash entries created by "git stash -S", if you want to test any of
them (because what is in these stash entries did not exist without
other working tree changes, and couldn't have been tested in the
working tree standalone by definition), you can "git stash pop" such
a stash entry created by "git stash -S" and then "git stash -k" to
materialize what was in the stash alone in the working tree to test
_later_ (as opposed to testing _first_; in the "git stash -k"
workflow, you'd collect "good bits" in the index with "add -p"
first, then "clear the remaining cruft" with "git stash -k" to test
it first, and take the cruft back with "git stash pop").

So in short, I do not think I am strongly opposed to "git stash -S"
existing, since I did find one use case story that it could be used,
but I do think it is redundant and unnecessary.

IOW, "git stash -k" followed by "git stash" and "git stash pop" the
one created with "git stash -k" would be an equivalent operation to
this new "git stash -S".  But the price of being able to combine
these three operations into one is that the user cannot have the
state after "stash -k" in the working tree to inspect, and I cannot
shake the feeling that this new "feature" is like a tail wagging a
dog.  If the "goal" is to "create a stash entry out of what is in
the index", then "stash -S" is a one-step handy tool that directly
achieves that "goal", but that "goal" does not smell like a useful
"goal" in the first place.  To "create a commit by sifting mixed
changes in the working tree with 'add -p' and then gaining a chance
to do a clean and final testing" would be the "goal" of "stash -k",
and that I can see a clear benefit.  Contrasting to that, I am not
so sure about "stash -S".  It would be another way to eventually do
the same thing but along a more roundabout route.

So, I dunno.
Sergey Organov Oct. 12, 2021, 4:07 p.m. UTC | #7
Junio C Hamano <gitster@pobox.com> writes:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> More importantly...
>>
>> Whenever I think about a new "feature", I try to come up with a
>> story in which the feature effectively improves the end-user's life,
>> how it fits in the larger picture, and enables something that is
>> hard to do by combining other tools.
>>
>> The kind of "story" I would aim for is like this.  Suppose we were
>> selling not "git stash -S" but "git stash -k". ...
>

[...]

> So in short, I do not think I am strongly opposed to "git stash -S"
> existing, since I did find one use case story that it could be used,
> but I do think it is redundant and unnecessary.

Redundant? Yes. Unnecessary? Yes. Useful? Yes. ;-)

I took the steps to propose the new feature after yet another round of
"how do I quickly store this tiny bit of changes I just figured I need
for later, out of bunch of VIWIP changes?"

  git stash --staged

is exactly the (currently missing) answer for me, as I have pretty
interactive tool to stage diff chunks always handy.

What's your answer, I wonder?

That said, I'm also curious what story, if any, do you have for 'git
stash --patch', as exactly the same story should be applicable to
proposed 'git stash --staged', as far as I can see.

Thanks,
-- Sergey Organov
Junio C Hamano Oct. 12, 2021, 5:28 p.m. UTC | #8
Sergey Organov <sorganov@gmail.com> writes:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> Junio C Hamano <gitster@pobox.com> writes:
>>
>>> More importantly...
>>>
>>> Whenever I think about a new "feature", I try to come up with a
>>> story in which the feature effectively improves the end-user's life,
>>> how it fits in the larger picture, and enables something that is
>>> hard to do by combining other tools.
>>>
>>> The kind of "story" I would aim for is like this.  Suppose we were
>>> selling not "git stash -S" but "git stash -k". ...
>>
>
> [...]
>
>> So in short, I do not think I am strongly opposed to "git stash -S"
>> existing, since I did find one use case story that it could be used,
>> but I do think it is redundant and unnecessary.
>
> Redundant? Yes. Unnecessary? Yes. Useful? Yes. ;-)
>
> I took the steps to propose the new feature after yet another round of
> "how do I quickly store this tiny bit of changes I just figured I need
> for later, out of bunch of VIWIP changes?"
>
>   git stash --staged
>
> is exactly the (currently missing) answer for me, as I have pretty
> interactive tool to stage diff chunks always handy.
>
> What's your answer, I wonder?

I am the one who questions the usefulness of "stash --staged" and
thinks "add -p", "stash -k", test, "commit" is a much better way to
solve the "we have a messy working tree and we want to create a
clean multi-step end result out of it" problem.

I consider "stash --staged" as a solution in search of a problem, so
you'd need to ask somebody else for a problem that "stash --staged"
is suitable for.

And "I want to stash away this tiny bit" is better solved by *not*
doing "git add" it to the index and then stashing.  Rather, I'd just
do "commit" so that I can "rebase -i" to reorganize these bits
later.  Of course, to test the "tiny bit" standalone, I may use
"stash -k" first, but do not see such a senario shows the merit of
using "stash --staged" over other tools.

> That said, I'm also curious what story, if any, do you have for 'git
> stash --patch', as exactly the same story should be applicable to
> proposed 'git stash --staged', as far as I can see.

"stash --patch" is also "Meh" from my point of view.  I do not
strongly object to its existence, it may be a OK tool for a small
scale use, but I suspect it would be more frustrating than helpful
to users when applied in a larger workflow story, just like I view
"git stash --staged".

Thanks.
Sergey Organov Oct. 12, 2021, 6:25 p.m. UTC | #9
Junio C Hamano <gitster@pobox.com> writes:

> Sergey Organov <sorganov@gmail.com> writes:
>
>> Junio C Hamano <gitster@pobox.com> writes:
>>
>>> Junio C Hamano <gitster@pobox.com> writes:
>>>
>>>> More importantly...
>>>>
>>>> Whenever I think about a new "feature", I try to come up with a
>>>> story in which the feature effectively improves the end-user's life,
>>>> how it fits in the larger picture, and enables something that is
>>>> hard to do by combining other tools.
>>>>
>>>> The kind of "story" I would aim for is like this.  Suppose we were
>>>> selling not "git stash -S" but "git stash -k". ...
>>>
>>
>> [...]
>>
>>> So in short, I do not think I am strongly opposed to "git stash -S"
>>> existing, since I did find one use case story that it could be used,
>>> but I do think it is redundant and unnecessary.
>>
>> Redundant? Yes. Unnecessary? Yes. Useful? Yes. ;-)
>>
>> I took the steps to propose the new feature after yet another round of
>> "how do I quickly store this tiny bit of changes I just figured I need
>> for later, out of bunch of VIWIP changes?"
>>
>>   git stash --staged
>>
>> is exactly the (currently missing) answer for me, as I have pretty
>> interactive tool to stage diff chunks always handy.
>>
>> What's your answer, I wonder?
>
> I am the one who questions the usefulness of "stash --staged" and
> thinks "add -p", "stash -k", test, "commit" is a much better way to
> solve the "we have a messy working tree and we want to create a
> clean multi-step end result out of it" problem

I don't want to create a multi-step result out of it, if it means a
series of commits. The question is about a change that is *unrelated* to
the series I'm supposedly doing.

>
> I consider "stash --staged" as a solution in search of a problem, so
> you'd need to ask somebody else for a problem that "stash --staged"
> is suitable for.

I didn't ask you what --staged is suitable for, sorry. I asked how do
you solve the problem of saving an *entirely unrelated* subset of
changes for future use?

If the answer is "I don't have such problem", it's OK with me, but my
point is that I, and at least a few others, seem to have such a problem
frequently enough to justify introduction of the --staged option.

>
> And "I want to stash away this tiny bit" is better solved by *not*
> doing "git add" it to the index and then stashing.  Rather, I'd just
> do "commit" so that I can "rebase -i" to reorganize these bits
> later.  Of course, to test the "tiny bit" standalone, I may use
> "stash -k" first, but do not see such a senario shows the merit of
> using "stash --staged" over other tools.

That is a good solution for *different* problem. The changes I want to
stash-out supposedly don't belong to the series of changes currently
being worked on *at all*, and I don't want to test them right now as I'm
working on entirely unrelated set of problems and don't want to get
side-tracked.

So, the analog here is not using "stage -k"->test->commit cycle, it's
rather temporary switching to another branch and committing there, like
this:

 <hack, hack, hack...>
 <notice unrelated problem, give it a quick fix and stage it>
 $ git checkout -b tmp-fix-bla-bla
 $ git commit -m "Will have to look at bla-bla later"
 $ git checkout -
 <hack continues, probably using stash -k and rebasing as needed>
 <... time passes... >
 $ git switch some-branch
 $ git cherry-pick -n tmp-fix-bla-bla
 <... continue to work on the bla-bla fix ...>

See? But now, we already have such a wonderful place for temporary
states called "stash". Why should it be so hard to "commit" right to the
stash instead of stomping around and then house-keeping of these
temporary non-branches? That's what "stash --staged" is suitable for,
not for creating clean sequence of commits out of a mess, where "stash
-k" indeed shines.

>
>> That said, I'm also curious what story, if any, do you have for 'git
>> stash --patch', as exactly the same story should be applicable to
>> proposed 'git stash --staged', as far as I can see.
>
> "stash --patch" is also "Meh" from my point of view.  I do not
> strongly object to its existence, it may be a OK tool for a small
> scale use, but I suspect it would be more frustrating than helpful
> to users when applied in a larger workflow story, just like I view
> "git stash --staged".

I see, thank you for clarification.

Thanks,
-- Sergey Organov
Junio C Hamano Oct. 13, 2021, 4:48 a.m. UTC | #10
Sergey Organov <sorganov@gmail.com> writes:

> I didn't ask you what --staged is suitable for, sorry. I asked how do
> you solve the problem of saving an *entirely unrelated* subset of
> changes for future use?

Ah, OK.  I do not think I would "git add" unrelated pieces in the
first place, so "add -p && stash --staged" as a way to stash away
such a change would be quite foreign concept in my workflow.

IOW, I'd concentrate on finishing the step I am working on, leaving
unrelated changes in the working tree, and when I came to a good
stopping point, I'd do the "stash -k && test && commit" dance,
followed by "stash pop".  At that point,the working tree would have
only unrelated changes that I can stash away with "stash save".

So I guess perhaps your "no such problem for me" is the closest?

Thanks.
Sergey Organov Oct. 13, 2021, 1:43 p.m. UTC | #11
Junio C Hamano <gitster@pobox.com> writes:

> Sergey Organov <sorganov@gmail.com> writes:
>
>> I didn't ask you what --staged is suitable for, sorry. I asked how do
>> you solve the problem of saving an *entirely unrelated* subset of
>> changes for future use?
>
> Ah, OK.  I do not think I would "git add" unrelated pieces in the
> first place, so "add -p && stash --staged" as a way to stash away
> such a change would be quite foreign concept in my workflow.
>
> IOW, I'd concentrate on finishing the step I am working on, leaving
> unrelated changes in the working tree, and when I came to a good
> stopping point, I'd do the "stash -k && test && commit" dance,
> followed by "stash pop".  At that point,the working tree would have
> only unrelated changes that I can stash away with "stash save".
>
> So I guess perhaps your "no such problem for me" is the closest?

Yep, looks like it is.

For me the problem with the approach you've adopted is that these
unrelated changes distract my attention every time I select what is to
be in the next clean commit, and it gets annoying over time.

Thanks,
-- Sergey Organov
diff mbox series

Patch

diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt
index be6084ccefbe..6e15f4752576 100644
--- a/Documentation/git-stash.txt
+++ b/Documentation/git-stash.txt
@@ -13,7 +13,7 @@  SYNOPSIS
 'git stash' drop [-q|--quiet] [<stash>]
 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
 'git stash' branch <branchname> [<stash>]
-'git stash' [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
+'git stash' [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]
 	     [-u|--include-untracked] [-a|--all] [-m|--message <message>]
 	     [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	     [--] [<pathspec>...]]
@@ -47,7 +47,7 @@  stash index (e.g. the integer `n` is equivalent to `stash@{n}`).
 COMMANDS
 --------
 
-push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]::
+push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]::
 
 	Save your local modifications to a new 'stash entry' and roll them
 	back to HEAD (in the working tree and in the index).
@@ -60,7 +60,7 @@  subcommand from making an unwanted stash entry.  The two exceptions to this
 are `stash -p` which acts as alias for `stash push -p` and pathspec elements,
 which are allowed after a double hyphen `--` for disambiguation.
 
-save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
+save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
 
 	This option is deprecated in favour of 'git stash push'.  It
 	differs from "stash push" in that it cannot take pathspec.
@@ -205,6 +205,16 @@  to learn how to operate the `--patch` mode.
 The `--patch` option implies `--keep-index`.  You can use
 `--no-keep-index` to override this.
 
+-S::
+--staged::
+	This option is only valid for `push` and `save` commands.
++
+Stash only the changes that are currently staged. This is similar to
+basic `git commit` except the state is committed to the stash instead
+of current branch.
++
+The `--patch` option has priority over this one.
+
 --pathspec-from-file=<file>::
 	This option is only valid for `push` command.
 +
@@ -341,6 +351,24 @@  $ edit/build/test remaining parts
 $ git commit foo -m 'Remaining parts'
 ----------------------------------------------------------------
 
+Saving unrelated changes for future use::
+
+When you are in the middle of massive changes and you find some
+unrelated issue that you don't want to forget to fix, you can do the
+change(s), stage them, and use `git stash push --staged` to stash them
+out for future use. This is similar to committing the staged changes,
+only the commit ends-up being in the stash and not on the current branch.
++
+----------------------------------------------------------------
+# ... hack hack hack ...
+$ git add --patch foo           # add unrelated changes to the index
+$ git stash push --staged       # save these changes to the stash
+# ... hack hack hack, finish curent changes ...
+$ git commit -m 'Massive'       # commit fully tested changes
+$ git switch fixup-branch       # switch to another branch
+$ git stash pop                 # to finish work on the saved changes
+----------------------------------------------------------------
+
 Recovering stash entries that were cleared/dropped erroneously::
 
 If you mistakenly drop or clear stash entries, they cannot be recovered
diff --git a/builtin/stash.c b/builtin/stash.c
index 8f42360ca913..97bf274627d8 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -27,11 +27,11 @@  static const char * const git_stash_usage[] = {
 	N_("git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]"),
 	N_("git stash branch <branchname> [<stash>]"),
 	"git stash clear",
-	N_("git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
+	N_("git stash [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n"
 	   "          [-u|--include-untracked] [-a|--all] [-m|--message <message>]\n"
 	   "          [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "          [--] [<pathspec>...]]"),
-	N_("git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
+	N_("git stash save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n"
 	   "          [-u|--include-untracked] [-a|--all] [<message>]"),
 	NULL
 };
@@ -1116,6 +1116,38 @@  static int save_untracked_files(struct stash_info *info, struct strbuf *msg,
 	return ret;
 }
 
+static int stash_staged(struct stash_info *info, const struct pathspec *ps,
+		       struct strbuf *out_patch, int quiet)
+{
+	int ret = 0;
+	struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
+	struct index_state istate = { NULL };
+
+	if (write_index_as_tree(&info->w_tree, &istate, the_repository->index_file,
+				0, NULL)) {
+		ret = -1;
+		goto done;
+	}
+
+	cp_diff_tree.git_cmd = 1;
+	strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD",
+		     oid_to_hex(&info->w_tree), "--", NULL);
+	if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) {
+		ret = -1;
+		goto done;
+	}
+
+	if (!out_patch->len) {
+		if (!quiet)
+			fprintf_ln(stderr, _("No changes selected"));
+		ret = 1;
+	}
+
+done:
+	discard_index(&istate);
+	return ret;
+}
+
 static int stash_patch(struct stash_info *info, const struct pathspec *ps,
 		       struct strbuf *out_patch, int quiet)
 {
@@ -1242,7 +1274,7 @@  static int stash_working_tree(struct stash_info *info, const struct pathspec *ps
 }
 
 static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
-			   int include_untracked, int patch_mode,
+			   int include_untracked, int patch_mode, int only_staged,
 			   struct stash_info *info, struct strbuf *patch,
 			   int quiet)
 {
@@ -1321,6 +1353,16 @@  static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
 		} else if (ret > 0) {
 			goto done;
 		}
+	} else if (only_staged) {
+		ret = stash_staged(info, ps, patch, quiet);
+		if (ret < 0) {
+			if (!quiet)
+				fprintf_ln(stderr, _("Cannot save the current "
+						     "staged state"));
+			goto done;
+		} else if (ret > 0) {
+			goto done;
+		}
 	} else {
 		if (stash_working_tree(info, ps)) {
 			if (!quiet)
@@ -1379,7 +1421,7 @@  static int create_stash(int argc, const char **argv, const char *prefix)
 	if (!check_changes_tracked_files(&ps))
 		return 0;
 
-	ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, &info,
+	ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, 0, &info,
 			      NULL, 0);
 	if (!ret)
 		printf_ln("%s", oid_to_hex(&info.w_commit));
@@ -1389,7 +1431,7 @@  static int create_stash(int argc, const char **argv, const char *prefix)
 }
 
 static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
-			 int keep_index, int patch_mode, int include_untracked)
+			 int keep_index, int patch_mode, int include_untracked, int only_staged)
 {
 	int ret = 0;
 	struct stash_info info;
@@ -1407,6 +1449,17 @@  static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 		goto done;
 	}
 
+	/* --patch overrides --staged */
+	if (patch_mode)
+		only_staged = 0;
+
+	if (only_staged && include_untracked) {
+		fprintf_ln(stderr, _("Can't use --staged and --include-untracked"
+				     " or --all at the same time"));
+		ret = -1;
+		goto done;
+	}
+
 	read_cache_preload(NULL);
 	if (!include_untracked && ps->nr) {
 		int i;
@@ -1447,7 +1500,7 @@  static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 
 	if (stash_msg)
 		strbuf_addstr(&stash_msg_buf, stash_msg);
-	if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
+	if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, only_staged,
 			    &info, &patch, quiet)) {
 		ret = -1;
 		goto done;
@@ -1464,7 +1517,7 @@  static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 		printf_ln(_("Saved working directory and index state %s"),
 			  stash_msg_buf.buf);
 
-	if (!patch_mode) {
+	if (!(patch_mode || only_staged)) {
 		if (include_untracked && !ps->nr) {
 			struct child_process cp = CHILD_PROCESS_INIT;
 
@@ -1581,6 +1634,7 @@  static int push_stash(int argc, const char **argv, const char *prefix,
 {
 	int force_assume = 0;
 	int keep_index = -1;
+	int only_staged = 0;
 	int patch_mode = 0;
 	int include_untracked = 0;
 	int quiet = 0;
@@ -1591,6 +1645,8 @@  static int push_stash(int argc, const char **argv, const char *prefix,
 	struct option options[] = {
 		OPT_BOOL('k', "keep-index", &keep_index,
 			 N_("keep index")),
+		OPT_BOOL('S', "staged", &only_staged,
+			 N_("stash staged changes only")),
 		OPT_BOOL('p', "patch", &patch_mode,
 			 N_("stash in patch mode")),
 		OPT__QUIET(&quiet, N_("quiet mode")),
@@ -1629,6 +1685,9 @@  static int push_stash(int argc, const char **argv, const char *prefix,
 		if (patch_mode)
 			die(_("--pathspec-from-file is incompatible with --patch"));
 
+		if (only_staged)
+			die(_("--pathspec-from-file is incompatible with --staged"));
+
 		if (ps.nr)
 			die(_("--pathspec-from-file is incompatible with pathspec arguments"));
 
@@ -1640,12 +1699,13 @@  static int push_stash(int argc, const char **argv, const char *prefix,
 	}
 
 	return do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
-			     include_untracked);
+			     include_untracked, only_staged);
 }
 
 static int save_stash(int argc, const char **argv, const char *prefix)
 {
 	int keep_index = -1;
+	int only_staged = 0;
 	int patch_mode = 0;
 	int include_untracked = 0;
 	int quiet = 0;
@@ -1656,6 +1716,8 @@  static int save_stash(int argc, const char **argv, const char *prefix)
 	struct option options[] = {
 		OPT_BOOL('k', "keep-index", &keep_index,
 			 N_("keep index")),
+		OPT_BOOL('S', "staged", &only_staged,
+			 N_("stash in patch mode")),
 		OPT_BOOL('p', "patch", &patch_mode,
 			 N_("stash in patch mode")),
 		OPT__QUIET(&quiet, N_("quiet mode")),
@@ -1677,7 +1739,7 @@  static int save_stash(int argc, const char **argv, const char *prefix)
 
 	memset(&ps, 0, sizeof(ps));
 	ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
-			    patch_mode, include_untracked);
+			    patch_mode, include_untracked, only_staged);
 
 	strbuf_release(&stash_msg_buf);
 	return ret;
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 873aa56e359d..18ea885412b8 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -288,6 +288,17 @@  test_expect_success 'stash --no-keep-index' '
 	test bar,bar2 = $(cat file),$(cat file2)
 '
 
+test_expect_success 'stash --staged' '
+	echo bar3 >file &&
+	echo bar4 >file2 &&
+	git add file2 &&
+	git stash --staged &&
+	test bar3,bar2 = $(cat file),$(cat file2) &&
+	git reset --hard &&
+	git stash pop &&
+	test bar,bar4 = $(cat file),$(cat file2)
+'
+
 test_expect_success 'dont assume push with non-option args' '
 	test_must_fail git stash -q drop 2>err &&
 	test_i18ngrep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err