Thread (4 messages) 4 messages, 3 authors, 3h ago
HOTtoday

[PATCH v2] stash: expose untracked modes in create

From: Kazumasa Shigeta <hidden>
Date: 2026-10-01 04:22:02
Subsystem: documentation, the rest · Maintainers: Jonathan Corbet, Linus Torvalds

`git stash create` always passes zero for the include_untracked parameter
of do_create_stash(), even though that helper already supports untracked
and ignored files and stash push/save expose those modes as
-u/--include-untracked and -a/--all.

Teach create to accept the same options and pass the existing mode
through. Unlike push/save, create continues to only create objects: it
does not update refs/stash, reset the index, or clean the working tree.

Use parse_options() for the new options and stop parsing at the first
non-option message word. This keeps option-like tokens after the message
as message text, while leading option-like arguments now follow Git's
normal option parsing. In particular, unknown or malformed leading
options are rejected instead of silently becoming a message, short
options may be combined, and `--` can be used when a message itself
begins with a dash.

Keep create's existing no-change behavior: detect the usual no-change
case before do_create_stash() refreshes and writes the index, and return
success without printing an object name. If do_create_stash() still
reports its internal "nothing to create" result, map that to create's
public success status.

This follows the stash subcommand exit-status convention established by
786fc390465f (stash: reserve exit status 1 for conflicts, 2026-09-03):
subcommands return 0 on success, negative values on failure, and status 1
when applying a stash results in conflicts. cmd_stash() maps negative
subcommand failures to 128.

9ca6326dff29 (stash: refactor stash_create, 2017-02-19) added the
internal include-untracked path while intentionally leaving the user
interface for "git stash create" unchanged. Reuse that machinery and
the existing INCLUDE_ALL_FILES mode rather than adding a separate stash
creation path.

Add coverage for short and long aliases, combined short options, the
untracked/ignored boundary including an ignored-only worktree, option
parsing and dash-leading messages, no-change behavior, and preservation
of refs/stash, the index state, and the working tree.

Signed-off-by: Kazumasa Shigeta <redacted>
---
 Documentation/git-stash.adoc | 19 ++++++---
 builtin/stash.c              | 48 +++++++++++++++++----
 t/t3903-stash.sh             | 83 ++++++++++++++++++++++++++++++++++++
 3 files changed, 135 insertions(+), 15 deletions(-)
diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index fc6a9a0..d343a75 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -21,7 +21,7 @@ git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | -
 git stash save [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
            [-u | --include-untracked] [-a | --all] [<message>]
 git stash clear
-git stash create [<message>]
+git stash create [-u | --include-untracked] [-a | --all] [--] [<message>]
 git stash store [(-m | --message) <message>] [-q | --quiet] <commit>
 git stash export (--print | --to-ref <ref>) [<stash>...]
 git stash import <commit>
@@ -138,10 +138,13 @@ with no conflicts.
 `drop [-q | --quiet] [<stash>]`::
 	Remove a single stash entry from the list of stash entries.
 
-`create`::
+`create [-u | --include-untracked] [-a | --all] [--]`::
 	Create a stash entry (which is a regular commit object) and
 	return its object name, without storing it anywhere in the ref
-	namespace.
+	namespace.  The `--include-untracked` option includes untracked
+	files, while `--all` also includes ignored files, without modifying
+	the working tree.  If `<message>` begins with a dash, use `--` to
+	separate it from the options.
 	This is intended to be useful for scripts.  It is probably not
 	the command you want to use; see "push" above.
 
@@ -167,10 +170,11 @@ OPTIONS
 -------
 `-a`::
 `--all`::
-	This option is only valid for `push` and `save` commands.
+	When used with the `push` and `save` commands, all ignored and
+	untracked files are also stashed and then cleaned up with `git clean`.
 +
-All ignored and untracked files are also stashed and then cleaned
-up with `git clean`.
+When used with the `create` command, ignored and untracked files are included
+in the stash entry without modifying the working tree.
 
 `-u`::
 `--include-untracked`::
@@ -179,6 +183,9 @@ up with `git clean`.
 	all untracked files are also stashed and then cleaned up with
 	`git clean`.
 +
+When used with the `create` command, untracked files are included in the
+stash entry without modifying the working tree.
++
 When used with the `show` command, show the untracked files in the stash
 entry as part of the diff.
 
diff --git a/builtin/stash.c b/builtin/stash.c
index 7a98434..ec2b5e7 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -59,7 +59,7 @@
 	N_("git stash save [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]\n" \
 	   "          [-u | --include-untracked] [-a | --all] [<message>]")
 #define BUILTIN_STASH_CREATE_USAGE \
-	N_("git stash create [<message>]")
+	N_("git stash create [-u | --include-untracked] [-a | --all] [--] [<message>]")
 #define BUILTIN_STASH_EXPORT_USAGE \
 	N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
 #define BUILTIN_STASH_IMPORT_USAGE \
@@ -119,6 +119,11 @@ static const char * const git_stash_clear_usage[] = {
 	NULL
 };
 
+static const char * const git_stash_create_usage[] = {
+	BUILTIN_STASH_CREATE_USAGE,
+	NULL
+};
+
 static const char * const git_stash_store_usage[] = {
 	BUILTIN_STASH_STORE_USAGE,
 	NULL
@@ -1643,26 +1648,51 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
 	return ret;
 }
 
-static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
+static int create_stash(int argc, const char **argv, const char *prefix,
 			struct repository *repo UNUSED)
 {
-	int ret;
+	int ret = 0;
+	int include_untracked = 0;
+	struct option options[] = {
+		OPT_BOOL('u', "include-untracked", &include_untracked,
+			 N_("include untracked files in stash")),
+		OPT_SET_INT('a', "all", &include_untracked,
+			    N_("include ignored files in stash"),
+			    INCLUDE_ALL_FILES),
+		OPT_END()
+	};
 	struct strbuf stash_msg_buf = STRBUF_INIT;
+	struct strbuf untracked_files = STRBUF_INIT;
 	struct stash_info info = STASH_INFO_INIT;
 	struct pathspec ps;
 
-	/* Starting with argv[1], since argv[0] is "create" */
-	strbuf_join_argv(&stash_msg_buf, argc - 1, ++argv, ' ');
+	argc = parse_options(argc, argv, prefix, options,
+			     git_stash_create_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	strbuf_join_argv(&stash_msg_buf, argc, argv, ' ');
 
 	memset(&ps, 0, sizeof(ps));
-	if (!check_changes_tracked_files(&ps))
-		return 0;
+	/*
+	 * Preserve "stash create"'s successful no-change behavior before
+	 * do_create_stash() refreshes and writes the index.
+	 */
+	if (!check_changes(&ps, include_untracked, &untracked_files))
+		goto done;
 
-	ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, NULL, 0, &info,
-			      NULL, 0);
+	ret = do_create_stash(&ps, &stash_msg_buf, include_untracked, 0, NULL,
+			      0, &info, NULL, 0);
+	/*
+	 * Status 1 is reserved for conflicts when applying a stash.
+	 * do_create_stash() uses it internally for "nothing to create", so
+	 * translate that sentinel to create's public success status.
+	 */
 	if (!ret)
 		printf_ln("%s", oid_to_hex(&info.w_commit));
+	else if (ret == 1)
+		ret = 0;
 
+done:
+	strbuf_release(&untracked_files);
 	free_stash_info(&info);
 	strbuf_release(&stash_msg_buf);
 	return ret;
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 7211586..1f660ca 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -1179,6 +1179,89 @@ test_expect_success 'create with multiple arguments for the message' '
 	test_cmp expect actual
 '
 
+test_expect_success 'create with untracked options' '
+	test_when_finished "rm -rf create-options" &&
+	git init create-options &&
+	test_commit -C create-options base tracked base &&
+	test_commit -C create-options ignore .gitignore ignored &&
+
+	git -C create-options stash create -u >actual &&
+	test_must_be_empty actual &&
+	git -C create-options stash create -a >actual &&
+	test_must_be_empty actual &&
+
+	echo untracked >create-options/untracked &&
+	echo ignored >create-options/ignored &&
+	git -C create-options diff >before-worktree &&
+	git -C create-options diff --cached >before-index &&
+	git -C create-options status --porcelain=v1 --ignored >before-status &&
+
+	short=$(git -C create-options stash create -u "create options") &&
+	long=$(git -C create-options stash create --include-untracked "create options") &&
+	test "$(git -C create-options rev-parse "$short^3^{tree}")" = "$(git -C create-options rev-parse "$long^3^{tree}")" &&
+	echo untracked >expect &&
+	git -C create-options show "$short^3:untracked" >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C create-options cat-file -e "$short^3:ignored" &&
+
+	short=$(git -C create-options stash create -a "create options") &&
+	long=$(git -C create-options stash create --all "create options") &&
+	cluster=$(git -C create-options stash create -ua "create options") &&
+	test "$(git -C create-options rev-parse "$short^3^{tree}")" = "$(git -C create-options rev-parse "$long^3^{tree}")" &&
+	test "$(git -C create-options rev-parse "$short^3^{tree}")" = "$(git -C create-options rev-parse "$cluster^3^{tree}")" &&
+	echo ignored >expect &&
+	git -C create-options show "$short^3:ignored" >actual &&
+	test_cmp expect actual &&
+
+	git -C create-options diff >after-worktree &&
+	git -C create-options diff --cached >after-index &&
+	git -C create-options status --porcelain=v1 --ignored >after-status &&
+	test_cmp before-worktree after-worktree &&
+	test_cmp before-index after-index &&
+	test_cmp before-status after-status &&
+	test_must_fail git -C create-options rev-parse --verify refs/stash >/dev/null 2>&1
+'
+
+test_expect_success 'create untracked modes with only ignored files' '
+	test_when_finished "rm -rf create-ignored-only" &&
+	git init create-ignored-only &&
+	test_commit -C create-ignored-only base tracked base &&
+	test_commit -C create-ignored-only ignore .gitignore ignored &&
+	echo ignored >create-ignored-only/ignored &&
+
+	git -C create-ignored-only stash create -u >actual &&
+	test_must_be_empty actual &&
+	stash=$(git -C create-ignored-only stash create -a) &&
+	test -n "$stash" &&
+	echo ignored >expect &&
+	git -C create-ignored-only show "$stash^3:ignored" >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'create option parsing and dash-leading messages' '
+	test_when_finished "rm -rf create-message-options" &&
+	git init create-message-options &&
+	test_commit -C create-message-options base tracked base &&
+	echo modified >>create-message-options/tracked &&
+	echo untracked >create-message-options/untracked &&
+
+	stash=$(git -C create-message-options stash create handle new -u flag) &&
+	echo "On main: handle new -u flag" >expect &&
+	git -C create-message-options show --pretty=%s -s "$stash" >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C create-message-options cat-file -e "$stash^3^{commit}" &&
+
+	test_must_fail git -C create-message-options stash create -f >out 2>err &&
+	test_grep "unknown switch" err &&
+	stash=$(git -C create-message-options stash create -- -f) &&
+	echo "On main: -f" >expect &&
+	git -C create-message-options show --pretty=%s -s "$stash" >actual &&
+	test_cmp expect actual &&
+
+	test_must_fail git -C create-message-options stash create \
+		--include-untracked=yes >out 2>err
+'
+
 test_expect_success 'create in a detached state' '
 	test_when_finished "git checkout main" &&
 	git checkout HEAD~1 &&
-- 
2.47.3
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help