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>

Sergey Organov committed Oct 18, 2021 at 19:09 UTC 41a28eb6c124bd06ca4a4fea9a1da3178a09467b
3 files changed +113 -12
Documentation/git-stash.txt
+31 -3
@@ -13,7 +13,7 @@ SYNOPSIS
13 'git stash' drop [-q|--quiet] [<stash>]
14 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
15 'git stash' branch <branchname> [<stash>]
16 -'git stash' [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
16 +'git stash' [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]
17 [-u|--include-untracked] [-a|--all] [-m|--message <message>]
18 [--pathspec-from-file=<file> [--pathspec-file-nul]]
19 [--] [<pathspec>...]]
@@ -47,7 +47,7 @@ stash index (e.g. the integer `n` is equivalent to `stash@{n}`).
47 COMMANDS
48 --------
49
50 -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>...]::
50 +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>...]::
51
52 Save your local modifications to a new 'stash entry' and roll them
53 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
60 are `stash -p` which acts as alias for `stash push -p` and pathspec elements,
61 which are allowed after a double hyphen `--` for disambiguation.
62
63 -save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
63 +save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
64
65 This option is deprecated in favour of 'git stash push'. It
66 differs from "stash push" in that it cannot take pathspec.
@@ -205,6 +205,16 @@ to learn how to operate the `--patch` mode.
205 The `--patch` option implies `--keep-index`. You can use
206 `--no-keep-index` to override this.
207
208 +-S::
209 +--staged::
210 + This option is only valid for `push` and `save` commands.
211 ++
212 +Stash only the changes that are currently staged. This is similar to
213 +basic `git commit` except the state is committed to the stash instead
214 +of current branch.
215 ++
216 +The `--patch` option has priority over this one.
217 +
218 --pathspec-from-file=<file>::
219 This option is only valid for `push` command.
220 +
@@ -341,6 +351,24 @@ $ edit/build/test remaining parts
351 $ git commit foo -m 'Remaining parts'
352 ----------------------------------------------------------------
353
354 +Saving unrelated changes for future use::
355 +
356 +When you are in the middle of massive changes and you find some
357 +unrelated issue that you don't want to forget to fix, you can do the
358 +change(s), stage them, and use `git stash push --staged` to stash them
359 +out for future use. This is similar to committing the staged changes,
360 +only the commit ends-up being in the stash and not on the current branch.
361 ++
362 +----------------------------------------------------------------
363 +# ... hack hack hack ...
364 +$ git add --patch foo # add unrelated changes to the index
365 +$ git stash push --staged # save these changes to the stash
366 +# ... hack hack hack, finish curent changes ...
367 +$ git commit -m 'Massive' # commit fully tested changes
368 +$ git switch fixup-branch # switch to another branch
369 +$ git stash pop # to finish work on the saved changes
370 +----------------------------------------------------------------
371 +
372 Recovering stash entries that were cleared/dropped erroneously::
373
374 If you mistakenly drop or clear stash entries, they cannot be recovered
builtin/stash.c
+71 -9
@@ -27,11 +27,11 @@ static const char * const git_stash_usage[] = {
27 N_("git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]"),
28 N_("git stash branch <branchname> [<stash>]"),
29 "git stash clear",
30 - N_("git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
30 + N_("git stash [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n"
31 " [-u|--include-untracked] [-a|--all] [-m|--message <message>]\n"
32 " [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
33 " [--] [<pathspec>...]]"),
34 - N_("git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
34 + N_("git stash save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n"
35 " [-u|--include-untracked] [-a|--all] [<message>]"),
36 NULL
37 };
@@ -1132,6 +1132,38 @@ done:
1132 return ret;
1133 }
1134
1135 +static int stash_staged(struct stash_info *info, const struct pathspec *ps,
1136 + struct strbuf *out_patch, int quiet)
1137 +{
1138 + int ret = 0;
1139 + struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
1140 + struct index_state istate = { NULL };
1141 +
1142 + if (write_index_as_tree(&info->w_tree, &istate, the_repository->index_file,
1143 + 0, NULL)) {
1144 + ret = -1;
1145 + goto done;
1146 + }
1147 +
1148 + cp_diff_tree.git_cmd = 1;
1149 + strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD",
1150 + oid_to_hex(&info->w_tree), "--", NULL);
1151 + if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) {
1152 + ret = -1;
1153 + goto done;
1154 + }
1155 +
1156 + if (!out_patch->len) {
1157 + if (!quiet)
1158 + fprintf_ln(stderr, _("No staged changes"));
1159 + ret = 1;
1160 + }
1161 +
1162 +done:
1163 + discard_index(&istate);
1164 + return ret;
1165 +}
1166 +
1167 static int stash_patch(struct stash_info *info, const struct pathspec *ps,
1168 struct strbuf *out_patch, int quiet)
1169 {
@@ -1258,7 +1290,7 @@ done:
1290 }
1291
1292 static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
1261 - int include_untracked, int patch_mode,
1293 + int include_untracked, int patch_mode, int only_staged,
1294 struct stash_info *info, struct strbuf *patch,
1295 int quiet)
1296 {
@@ -1337,6 +1369,16 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
1369 } else if (ret > 0) {
1370 goto done;
1371 }
1372 + } else if (only_staged) {
1373 + ret = stash_staged(info, ps, patch, quiet);
1374 + if (ret < 0) {
1375 + if (!quiet)
1376 + fprintf_ln(stderr, _("Cannot save the current "
1377 + "staged state"));
1378 + goto done;
1379 + } else if (ret > 0) {
1380 + goto done;
1381 + }
1382 } else {
1383 if (stash_working_tree(info, ps)) {
1384 if (!quiet)
@@ -1395,7 +1437,7 @@ static int create_stash(int argc, const char **argv, const char *prefix)
1437 if (!check_changes_tracked_files(&ps))
1438 return 0;
1439
1398 - ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, &info,
1440 + ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, 0, &info,
1441 NULL, 0);
1442 if (!ret)
1443 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)
1447 }
1448
1449 static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
1408 - int keep_index, int patch_mode, int include_untracked)
1450 + int keep_index, int patch_mode, int include_untracked, int only_staged)
1451 {
1452 int ret = 0;
1453 struct stash_info info;
@@ -1423,6 +1465,17 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
1465 goto done;
1466 }
1467
1468 + /* --patch overrides --staged */
1469 + if (patch_mode)
1470 + only_staged = 0;
1471 +
1472 + if (only_staged && include_untracked) {
1473 + fprintf_ln(stderr, _("Can't use --staged and --include-untracked"
1474 + " or --all at the same time"));
1475 + ret = -1;
1476 + goto done;
1477 + }
1478 +
1479 read_cache_preload(NULL);
1480 if (!include_untracked && ps->nr) {
1481 int i;
@@ -1463,7 +1516,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
1516
1517 if (stash_msg)
1518 strbuf_addstr(&stash_msg_buf, stash_msg);
1466 - if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
1519 + if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, only_staged,
1520 &info, &patch, quiet)) {
1521 ret = -1;
1522 goto done;
@@ -1480,7 +1533,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
1533 printf_ln(_("Saved working directory and index state %s"),
1534 stash_msg_buf.buf);
1535
1483 - if (!patch_mode) {
1536 + if (!(patch_mode || only_staged)) {
1537 if (include_untracked && !ps->nr) {
1538 struct child_process cp = CHILD_PROCESS_INIT;
1539
@@ -1598,6 +1651,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
1651 {
1652 int force_assume = 0;
1653 int keep_index = -1;
1654 + int only_staged = 0;
1655 int patch_mode = 0;
1656 int include_untracked = 0;
1657 int quiet = 0;
@@ -1608,6 +1662,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
1662 struct option options[] = {
1663 OPT_BOOL('k', "keep-index", &keep_index,
1664 N_("keep index")),
1665 + OPT_BOOL('S', "staged", &only_staged,
1666 + N_("stash staged changes only")),
1667 OPT_BOOL('p', "patch", &patch_mode,
1668 N_("stash in patch mode")),
1669 OPT__QUIET(&quiet, N_("quiet mode")),
@@ -1646,6 +1702,9 @@ static int push_stash(int argc, const char **argv, const char *prefix,
1702 if (patch_mode)
1703 die(_("--pathspec-from-file is incompatible with --patch"));
1704
1705 + if (only_staged)
1706 + die(_("--pathspec-from-file is incompatible with --staged"));
1707 +
1708 if (ps.nr)
1709 die(_("--pathspec-from-file is incompatible with pathspec arguments"));
1710
@@ -1657,12 +1716,13 @@ static int push_stash(int argc, const char **argv, const char *prefix,
1716 }
1717
1718 return do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
1660 - include_untracked);
1719 + include_untracked, only_staged);
1720 }
1721
1722 static int save_stash(int argc, const char **argv, const char *prefix)
1723 {
1724 int keep_index = -1;
1725 + int only_staged = 0;
1726 int patch_mode = 0;
1727 int include_untracked = 0;
1728 int quiet = 0;
@@ -1673,6 +1733,8 @@ static int save_stash(int argc, const char **argv, const char *prefix)
1733 struct option options[] = {
1734 OPT_BOOL('k', "keep-index", &keep_index,
1735 N_("keep index")),
1736 + OPT_BOOL('S', "staged", &only_staged,
1737 + N_("stash staged changes only")),
1738 OPT_BOOL('p', "patch", &patch_mode,
1739 N_("stash in patch mode")),
1740 OPT__QUIET(&quiet, N_("quiet mode")),
@@ -1694,7 +1756,7 @@ static int save_stash(int argc, const char **argv, const char *prefix)
1756
1757 memset(&ps, 0, sizeof(ps));
1758 ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
1697 - patch_mode, include_untracked);
1759 + patch_mode, include_untracked, only_staged);
1760
1761 strbuf_release(&stash_msg_buf);
1762 return ret;
t/t3903-stash.sh
+11
@@ -288,6 +288,17 @@ test_expect_success 'stash --no-keep-index' '
288 test bar,bar2 = $(cat file),$(cat file2)
289 '
290
291 +test_expect_success 'stash --staged' '
292 + echo bar3 >file &&
293 + echo bar4 >file2 &&
294 + git add file2 &&
295 + git stash --staged &&
296 + test bar3,bar2 = $(cat file),$(cat file2) &&
297 + git reset --hard &&
298 + git stash pop &&
299 + test bar,bar4 = $(cat file),$(cat file2)
300 +'
301 +
302 test_expect_success 'dont assume push with non-option args' '
303 test_must_fail git stash -q drop 2>err &&
304 test_i18ngrep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err