stash: implement '--staged' option for 'push' and 'save'

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 use 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>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Sergey Organov 2021-10-18 19:09:06 +03:00 committed by Junio C Hamano
parent f443b226ca
commit 41a28eb6c1
3 changed files with 113 additions and 12 deletions

View File

@ -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

View File

@ -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
};
@ -1132,6 +1132,38 @@ done:
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 staged changes"));
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)
{
@ -1258,7 +1290,7 @@ done:
}
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)
{
@ -1337,6 +1369,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)
@ -1395,7 +1437,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));
@ -1405,7 +1447,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;
@ -1423,6 +1465,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;
@ -1463,7 +1516,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;
@ -1480,7 +1533,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;
@ -1598,6 +1651,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;
@ -1608,6 +1662,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")),
@ -1646,6 +1702,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"));
@ -1657,12 +1716,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;
@ -1673,6 +1733,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 staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
OPT__QUIET(&quiet, N_("quiet mode")),
@ -1694,7 +1756,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;

View File

@ -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