Skip to content

Commit d3c7bf7

Browse files
Denton-Lgitster
authored andcommitted
stash show: teach --include-untracked and --only-untracked
Stash entries can be made with untracked files via `git stash push --include-untracked`. However, because the untracked files are stored in the third parent of the stash entry and not the stash entry itself, running `git stash show` does not include the untracked files as part of the diff. With --include-untracked, untracked paths, which are recorded in the third-parent if it exists, are shown in addition to the paths that have modifications between the stash base and the working tree in the stash. It is possible to manually craft a malformed stash entry where duplicate untracked files in the stash entry will mask tracked files. We detect and error out in that case via a custom unpack_trees() callback: stash_worktree_untracked_merge(). Also, teach stash the --only-untracked option which only shows the untracked files of a stash entry. This is similar to `git show stash^3` but it is nice to provide a convenient abstraction for it so that users do not have to think about the underlying implementation. Signed-off-by: Denton Liu <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 3e885f0 commit d3c7bf7

File tree

6 files changed

+197
-7
lines changed

6 files changed

+197
-7
lines changed

Documentation/git-stash.txt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SYNOPSIS
99
--------
1010
[verse]
1111
'git stash' list [<log-options>]
12-
'git stash' show [<diff-options>] [<stash>]
12+
'git stash' show [-u|--include-untracked|--only-untracked] [<diff-options>] [<stash>]
1313
'git stash' drop [-q|--quiet] [<stash>]
1414
'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
1515
'git stash' branch <branchname> [<stash>]
@@ -83,7 +83,7 @@ stash@{1}: On master: 9cc0589... Add git-stash
8383
The command takes options applicable to the 'git log'
8484
command to control what is shown and how. See linkgit:git-log[1].
8585

86-
show [<diff-options>] [<stash>]::
86+
show [-u|--include-untracked|--only-untracked] [<diff-options>] [<stash>]::
8787

8888
Show the changes recorded in the stash entry as a diff between the
8989
stashed contents and the commit back when the stash entry was first
@@ -160,10 +160,18 @@ up with `git clean`.
160160

161161
-u::
162162
--include-untracked::
163-
This option is only valid for `push` and `save` commands.
163+
--no-include-untracked::
164+
When used with the `push` and `save` commands,
165+
all untracked files are also stashed and then cleaned up with
166+
`git clean`.
167+
+
168+
When used with the `show` command, show the untracked files in the stash
169+
entry as part of the diff.
170+
171+
--only-untracked::
172+
This option is only valid for the `show` command.
164173
+
165-
All untracked files are also stashed and then cleaned up with
166-
`git clean`.
174+
Show only the untracked files in the stash entry as part of the diff.
167175

168176
--index::
169177
This option is only valid for `pop` and `apply` commands.

builtin/stash.c

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,33 @@ static int git_stash_config(const char *var, const char *value, void *cb)
787787
return git_diff_basic_config(var, value, cb);
788788
}
789789

790+
static void diff_include_untracked(const struct stash_info *info, struct diff_options *diff_opt)
791+
{
792+
const struct object_id *oid[] = { &info->w_commit, &info->u_tree };
793+
struct tree *tree[ARRAY_SIZE(oid)];
794+
struct tree_desc tree_desc[ARRAY_SIZE(oid)];
795+
struct unpack_trees_options unpack_tree_opt = { 0 };
796+
int i;
797+
798+
for (i = 0; i < ARRAY_SIZE(oid); i++) {
799+
tree[i] = parse_tree_indirect(oid[i]);
800+
if (parse_tree(tree[i]) < 0)
801+
die(_("failed to parse tree"));
802+
init_tree_desc(&tree_desc[i], tree[i]->buffer, tree[i]->size);
803+
}
804+
805+
unpack_tree_opt.head_idx = -1;
806+
unpack_tree_opt.src_index = &the_index;
807+
unpack_tree_opt.dst_index = &the_index;
808+
unpack_tree_opt.merge = 1;
809+
unpack_tree_opt.fn = stash_worktree_untracked_merge;
810+
811+
if (unpack_trees(ARRAY_SIZE(tree_desc), tree_desc, &unpack_tree_opt))
812+
die(_("failed to unpack trees"));
813+
814+
do_diff_cache(&info->b_commit, diff_opt);
815+
}
816+
790817
static int show_stash(int argc, const char **argv, const char *prefix)
791818
{
792819
int i;
@@ -795,14 +822,29 @@ static int show_stash(int argc, const char **argv, const char *prefix)
795822
struct rev_info rev;
796823
struct strvec stash_args = STRVEC_INIT;
797824
struct strvec revision_args = STRVEC_INIT;
825+
enum {
826+
UNTRACKED_NONE,
827+
UNTRACKED_INCLUDE,
828+
UNTRACKED_ONLY
829+
} show_untracked = UNTRACKED_NONE;
798830
struct option options[] = {
831+
OPT_SET_INT('u', "include-untracked", &show_untracked,
832+
N_("include untracked files in the stash"),
833+
UNTRACKED_INCLUDE),
834+
OPT_SET_INT_F(0, "only-untracked", &show_untracked,
835+
N_("only show untracked files in the stash"),
836+
UNTRACKED_ONLY, PARSE_OPT_NONEG),
799837
OPT_END()
800838
};
801839

802840
init_diff_ui_defaults();
803841
git_config(git_diff_ui_config, NULL);
804842
init_revisions(&rev, prefix);
805843

844+
argc = parse_options(argc, argv, prefix, options, git_stash_show_usage,
845+
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
846+
PARSE_OPT_KEEP_DASHDASH);
847+
806848
strvec_push(&revision_args, argv[0]);
807849
for (i = 1; i < argc; i++) {
808850
if (argv[i][0] != '-')
@@ -845,7 +887,17 @@ static int show_stash(int argc, const char **argv, const char *prefix)
845887

846888
rev.diffopt.flags.recursive = 1;
847889
setup_diff_pager(&rev.diffopt);
848-
diff_tree_oid(&info.b_commit, &info.w_commit, "", &rev.diffopt);
890+
switch (show_untracked) {
891+
case UNTRACKED_NONE:
892+
diff_tree_oid(&info.b_commit, &info.w_commit, "", &rev.diffopt);
893+
break;
894+
case UNTRACKED_ONLY:
895+
diff_root_tree_oid(&info.u_tree, "", &rev.diffopt);
896+
break;
897+
case UNTRACKED_INCLUDE:
898+
diff_include_untracked(&info, &rev.diffopt);
899+
break;
900+
}
849901
log_tree_diff_flush(&rev);
850902

851903
free_stash_info(&info);

contrib/completion/git-completion.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3051,7 +3051,7 @@ _git_stash ()
30513051
__gitcomp "--name-status --oneline --patch-with-stat"
30523052
;;
30533053
show,--*)
3054-
__gitcomp "$__git_diff_common_options"
3054+
__gitcomp "--include-untracked --only-untracked $__git_diff_common_options"
30553055
;;
30563056
branch,--*)
30573057
;;

t/t3905-stash-include-untracked.sh

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,110 @@ test_expect_success 'stash -u with globs' '
297297
test_path_is_missing untracked.txt
298298
'
299299

300+
test_expect_success 'stash show --include-untracked shows untracked files' '
301+
git reset --hard &&
302+
git clean -xf &&
303+
>untracked &&
304+
>tracked &&
305+
git add tracked &&
306+
empty_blob_oid=$(git rev-parse --short :tracked) &&
307+
git stash -u &&
308+
309+
cat >expect <<-EOF &&
310+
tracked | 0
311+
untracked | 0
312+
2 files changed, 0 insertions(+), 0 deletions(-)
313+
EOF
314+
git stash show --include-untracked >actual &&
315+
test_cmp expect actual &&
316+
git stash show -u >actual &&
317+
test_cmp expect actual &&
318+
git stash show --no-include-untracked --include-untracked >actual &&
319+
test_cmp expect actual &&
320+
git stash show --only-untracked --include-untracked >actual &&
321+
test_cmp expect actual &&
322+
323+
cat >expect <<-EOF &&
324+
diff --git a/tracked b/tracked
325+
new file mode 100644
326+
index 0000000..$empty_blob_oid
327+
diff --git a/untracked b/untracked
328+
new file mode 100644
329+
index 0000000..$empty_blob_oid
330+
EOF
331+
git stash show -p --include-untracked >actual &&
332+
test_cmp expect actual &&
333+
git stash show --include-untracked -p >actual &&
334+
test_cmp expect actual
335+
'
336+
337+
test_expect_success 'stash show --only-untracked only shows untracked files' '
338+
git reset --hard &&
339+
git clean -xf &&
340+
>untracked &&
341+
>tracked &&
342+
git add tracked &&
343+
empty_blob_oid=$(git rev-parse --short :tracked) &&
344+
git stash -u &&
345+
346+
cat >expect <<-EOF &&
347+
untracked | 0
348+
1 file changed, 0 insertions(+), 0 deletions(-)
349+
EOF
350+
git stash show --only-untracked >actual &&
351+
test_cmp expect actual &&
352+
git stash show --no-include-untracked --only-untracked >actual &&
353+
test_cmp expect actual &&
354+
git stash show --include-untracked --only-untracked >actual &&
355+
test_cmp expect actual &&
356+
357+
cat >expect <<-EOF &&
358+
diff --git a/untracked b/untracked
359+
new file mode 100644
360+
index 0000000..$empty_blob_oid
361+
EOF
362+
git stash show -p --only-untracked >actual &&
363+
test_cmp expect actual &&
364+
git stash show --only-untracked -p >actual &&
365+
test_cmp expect actual
366+
'
367+
368+
test_expect_success 'stash show --no-include-untracked cancels --{include,show}-untracked' '
369+
git reset --hard &&
370+
git clean -xf &&
371+
>untracked &&
372+
>tracked &&
373+
git add tracked &&
374+
git stash -u &&
375+
376+
cat >expect <<-EOF &&
377+
tracked | 0
378+
1 file changed, 0 insertions(+), 0 deletions(-)
379+
EOF
380+
git stash show --only-untracked --no-include-untracked >actual &&
381+
test_cmp expect actual &&
382+
git stash show --include-untracked --no-include-untracked >actual &&
383+
test_cmp expect actual
384+
'
385+
386+
test_expect_success 'stash show --include-untracked errors on duplicate files' '
387+
git reset --hard &&
388+
git clean -xf &&
389+
>tracked &&
390+
git add tracked &&
391+
tree=$(git write-tree) &&
392+
i_commit=$(git commit-tree -p HEAD -m "index on any-branch" "$tree") &&
393+
test_when_finished "rm -f untracked_index" &&
394+
u_commit=$(
395+
GIT_INDEX_FILE="untracked_index" &&
396+
export GIT_INDEX_FILE &&
397+
git update-index --add tracked &&
398+
u_tree=$(git write-tree) &&
399+
git commit-tree -m "untracked files on any-branch" "$u_tree"
400+
) &&
401+
w_commit=$(git commit-tree -p HEAD -p "$i_commit" -p "$u_commit" -m "WIP on any-branch" "$tree") &&
402+
test_must_fail git stash show --include-untracked "$w_commit" 2>err &&
403+
test_i18ngrep "worktree and untracked commit have duplicate entries: tracked" err
404+
'
405+
300406
test_done

unpack-trees.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,3 +2566,25 @@ int oneway_merge(const struct cache_entry * const *src,
25662566
}
25672567
return merged_entry(a, old, o);
25682568
}
2569+
2570+
/*
2571+
* Merge worktree and untracked entries in a stash entry.
2572+
*
2573+
* Ignore all index entries. Collapse remaining trees but make sure that they
2574+
* don't have any conflicting files.
2575+
*/
2576+
int stash_worktree_untracked_merge(const struct cache_entry * const *src,
2577+
struct unpack_trees_options *o)
2578+
{
2579+
const struct cache_entry *worktree = src[1];
2580+
const struct cache_entry *untracked = src[2];
2581+
2582+
if (o->merge_size != 2)
2583+
BUG("invalid merge_size: %d", o->merge_size);
2584+
2585+
if (worktree && untracked)
2586+
return error(_("worktree and untracked commit have duplicate entries: %s"),
2587+
super_prefixed(worktree->name));
2588+
2589+
return merged_entry(worktree ? worktree : untracked, NULL, o);
2590+
}

unpack-trees.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,7 @@ int bind_merge(const struct cache_entry * const *src,
114114
struct unpack_trees_options *o);
115115
int oneway_merge(const struct cache_entry * const *src,
116116
struct unpack_trees_options *o);
117+
int stash_worktree_untracked_merge(const struct cache_entry * const *src,
118+
struct unpack_trees_options *o);
117119

118120
#endif

0 commit comments

Comments
 (0)