Skip to content

Commit aafb754

Browse files
committed
Merge branch 'en/clean-nested-with-ignored'
"git clean" fixes. * en/clean-nested-with-ignored: dir: special case check for the possibility that pathspec is NULL clean: fix theoretical path corruption clean: rewrap overly long line clean: avoid removing untracked files in a nested git repository clean: disambiguate the definition of -d git-clean.txt: do not claim we will delete files with -n/--dry-run dir: add commentary explaining match_pathspec_item's return value dir: if our pathspec might match files under a dir, recurse into it dir: make the DO_MATCH_SUBMODULE code reusable for a non-submodule case dir: also check directories for matching pathspecs dir: fix off-by-one error in match_pathspec_item dir: fix typo in comment t7300: add testcases showing failure to clean specified pathspecs
2 parents 70bf0b7 + 69f272b commit aafb754

File tree

6 files changed

+134
-34
lines changed

6 files changed

+134
-34
lines changed

Documentation/git-clean.txt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@ are affected.
2626
OPTIONS
2727
-------
2828
-d::
29-
Remove untracked directories in addition to untracked files.
30-
If an untracked directory is managed by a different Git
31-
repository, it is not removed by default. Use -f option twice
32-
if you really want to remove such a directory.
29+
Normally, when no <path> is specified, git clean will not
30+
recurse into untracked directories to avoid removing too much.
31+
Specify -d to have it recurse into such directories as well.
32+
If any paths are specified, -d is irrelevant; all untracked
33+
files matching the specified paths (with exceptions for nested
34+
git directories mentioned under `--force`) will be removed.
3335

3436
-f::
3537
--force::
3638
If the Git configuration variable clean.requireForce is not set
3739
to false, 'git clean' will refuse to delete files or directories
38-
unless given -f, -n or -i. Git will refuse to delete directories
39-
with .git sub directory or file unless a second -f
40-
is given.
40+
unless given -f or -i. Git will refuse to modify untracked
41+
nested git repositories (directories with a .git subdirectory)
42+
unless a second -f is given.
4143

4244
-i::
4345
--interactive::

builtin/clean.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
158158

159159
*dir_gone = 1;
160160

161-
if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) && is_nonbare_repository_dir(path)) {
161+
if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
162+
is_nonbare_repository_dir(path)) {
162163
if (!quiet) {
163164
quote_path_relative(path->buf, prefix, &quoted);
164165
printf(dry_run ? _(msg_would_skip_git_dir) : _(msg_skip_git_dir),
@@ -946,9 +947,19 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
946947

947948
if (force > 1)
948949
rm_flags = 0;
950+
else
951+
dir.flags |= DIR_SKIP_NESTED_GIT;
949952

950953
dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
951954

955+
if (argc) {
956+
/*
957+
* Remaining args implies pathspecs specified, and we should
958+
* recurse within those.
959+
*/
960+
remove_directories = 1;
961+
}
962+
952963
if (remove_directories)
953964
dir.flags |= DIR_SHOW_IGNORED_TOO | DIR_KEEP_UNTRACKED_CONTENTS;
954965

@@ -1007,6 +1018,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
10071018
for_each_string_list_item(item, &del_list) {
10081019
struct stat st;
10091020

1021+
strbuf_reset(&abs_path);
10101022
if (prefix)
10111023
strbuf_addstr(&abs_path, prefix);
10121024

@@ -1040,7 +1052,6 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
10401052
printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
10411053
}
10421054
}
1043-
strbuf_reset(&abs_path);
10441055
}
10451056

10461057
strbuf_release(&abs_path);

dir.c

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
139139
* ":(icase)path" is treated as a pathspec full of
140140
* wildcard. In other words, only prefix is considered common
141141
* prefix. If the pathspec is abc/foo abc/bar, running in
142-
* subdir xyz, the common prefix is still xyz, not xuz/abc as
142+
* subdir xyz, the common prefix is still xyz, not xyz/abc as
143143
* in non-:(icase).
144144
*/
145145
GUARD_PATHSPEC(pathspec,
@@ -273,19 +273,30 @@ static int do_read_blob(const struct object_id *oid, struct oid_stat *oid_stat,
273273

274274
#define DO_MATCH_EXCLUDE (1<<0)
275275
#define DO_MATCH_DIRECTORY (1<<1)
276-
#define DO_MATCH_SUBMODULE (1<<2)
276+
#define DO_MATCH_LEADING_PATHSPEC (1<<2)
277277

278278
/*
279-
* Does 'match' match the given name?
280-
* A match is found if
279+
* Does the given pathspec match the given name? A match is found if
281280
*
282-
* (1) the 'match' string is leading directory of 'name', or
283-
* (2) the 'match' string is a wildcard and matches 'name', or
284-
* (3) the 'match' string is exactly the same as 'name'.
281+
* (1) the pathspec string is leading directory of 'name' ("RECURSIVELY"), or
282+
* (2) the pathspec string has a leading part matching 'name' ("LEADING"), or
283+
* (3) the pathspec string is a wildcard and matches 'name' ("WILDCARD"), or
284+
* (4) the pathspec string is exactly the same as 'name' ("EXACT").
285285
*
286-
* and the return value tells which case it was.
286+
* Return value tells which case it was (1-4), or 0 when there is no match.
287287
*
288-
* It returns 0 when there is no match.
288+
* It may be instructive to look at a small table of concrete examples
289+
* to understand the differences between 1, 2, and 4:
290+
*
291+
* Pathspecs
292+
* | a/b | a/b/ | a/b/c
293+
* ------+-----------+-----------+------------
294+
* a/b | EXACT | EXACT[1] | LEADING[2]
295+
* Names a/b/ | RECURSIVE | EXACT | LEADING[2]
296+
* a/b/c | RECURSIVE | RECURSIVE | EXACT
297+
*
298+
* [1] Only if DO_MATCH_DIRECTORY is passed; otherwise, this is NOT a match.
299+
* [2] Only if DO_MATCH_LEADING_PATHSPEC is passed; otherwise, not a match.
289300
*/
290301
static int match_pathspec_item(const struct index_state *istate,
291302
const struct pathspec_item *item, int prefix,
@@ -353,13 +364,14 @@ static int match_pathspec_item(const struct index_state *istate,
353364
item->nowildcard_len - prefix))
354365
return MATCHED_FNMATCH;
355366

356-
/* Perform checks to see if "name" is a super set of the pathspec */
357-
if (flags & DO_MATCH_SUBMODULE) {
367+
/* Perform checks to see if "name" is a leading string of the pathspec */
368+
if (flags & DO_MATCH_LEADING_PATHSPEC) {
358369
/* name is a literal prefix of the pathspec */
370+
int offset = name[namelen-1] == '/' ? 1 : 0;
359371
if ((namelen < matchlen) &&
360-
(match[namelen] == '/') &&
372+
(match[namelen-offset] == '/') &&
361373
!ps_strncmp(item, match, name, namelen))
362-
return MATCHED_RECURSIVELY;
374+
return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
363375

364376
/* name" doesn't match up to the first wild character */
365377
if (item->nowildcard_len < item->len &&
@@ -376,7 +388,7 @@ static int match_pathspec_item(const struct index_state *istate,
376388
* The submodules themselves will be able to perform more
377389
* accurate matching to determine if the pathspec matches.
378390
*/
379-
return MATCHED_RECURSIVELY;
391+
return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
380392
}
381393

382394
return 0;
@@ -497,7 +509,7 @@ int submodule_path_match(const struct index_state *istate,
497509
strlen(submodule_name),
498510
0, seen,
499511
DO_MATCH_DIRECTORY |
500-
DO_MATCH_SUBMODULE);
512+
DO_MATCH_LEADING_PATHSPEC);
501513
return matched;
502514
}
503515

@@ -1451,6 +1463,16 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
14511463
return path_none;
14521464

14531465
case index_nonexistent:
1466+
if (dir->flags & DIR_SKIP_NESTED_GIT) {
1467+
int nested_repo;
1468+
struct strbuf sb = STRBUF_INIT;
1469+
strbuf_addstr(&sb, dirname);
1470+
nested_repo = is_nonbare_repository_dir(&sb);
1471+
strbuf_release(&sb);
1472+
if (nested_repo)
1473+
return path_none;
1474+
}
1475+
14541476
if (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)
14551477
break;
14561478
if (exclude &&
@@ -1950,8 +1972,11 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
19501972
/* recurse into subdir if instructed by treat_path */
19511973
if ((state == path_recurse) ||
19521974
((state == path_untracked) &&
1953-
(dir->flags & DIR_SHOW_IGNORED_TOO) &&
1954-
(get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR))) {
1975+
(get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR) &&
1976+
((dir->flags & DIR_SHOW_IGNORED_TOO) ||
1977+
(pathspec &&
1978+
do_match_pathspec(istate, pathspec, path.buf, path.len,
1979+
baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)))) {
19551980
struct untracked_cache_dir *ud;
19561981
ud = lookup_untracked(dir->untracked, untracked,
19571982
path.buf + baselen,
@@ -1962,6 +1987,12 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
19621987
check_only, stop_at_first_file, pathspec);
19631988
if (subdir_state > dir_state)
19641989
dir_state = subdir_state;
1990+
1991+
if (pathspec &&
1992+
!match_pathspec(istate, pathspec, path.buf, path.len,
1993+
0 /* prefix */, NULL,
1994+
0 /* do NOT special case dirs */))
1995+
state = path_none;
19651996
}
19661997

19671998
if (check_only) {

dir.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ struct dir_struct {
156156
DIR_SHOW_IGNORED_TOO = 1<<5,
157157
DIR_COLLECT_KILLED_ONLY = 1<<6,
158158
DIR_KEEP_UNTRACKED_CONTENTS = 1<<7,
159-
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8
159+
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8,
160+
DIR_SKIP_NESTED_GIT = 1<<9
160161
} flags;
161162
struct dir_entry **entries;
162163
struct dir_entry **ignored;
@@ -211,8 +212,9 @@ int count_slashes(const char *s);
211212
* when populating the seen[] array.
212213
*/
213214
#define MATCHED_RECURSIVELY 1
214-
#define MATCHED_FNMATCH 2
215-
#define MATCHED_EXACTLY 3
215+
#define MATCHED_RECURSIVELY_LEADING_PATHSPEC 2
216+
#define MATCHED_FNMATCH 3
217+
#define MATCHED_EXACTLY 4
216218
int simple_length(const char *match);
217219
int no_wildcard(const char *string);
218220
char *common_prefix(const struct pathspec *pathspec);

t/t0050-filesystem.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,24 @@ $test_unicode 'merge (silent unicode normalization)' '
131131
git merge topic
132132
'
133133

134+
test_expect_success CASE_INSENSITIVE_FS 'checkout with no pathspec and a case insensitive fs' '
135+
git init repo &&
136+
(
137+
cd repo &&
138+
139+
>Gitweb &&
140+
git add Gitweb &&
141+
git commit -m "add Gitweb" &&
142+
143+
git checkout --orphan todo &&
144+
git reset --hard &&
145+
mkdir -p gitweb/subdir &&
146+
>gitweb/subdir/file &&
147+
git add gitweb &&
148+
git commit -m "add gitweb/subdir/file" &&
149+
150+
git checkout master
151+
)
152+
'
153+
134154
test_done

t/t7300-clean.sh

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ test_expect_success C_LOCALE_OUTPUT 'git clean with relative prefix' '
117117
would_clean=$(
118118
cd docs &&
119119
git clean -n ../src |
120+
grep part3 |
120121
sed -n -e "s|^Would remove ||p"
121122
) &&
122123
verbose test "$would_clean" = ../src/part3.c
@@ -129,6 +130,7 @@ test_expect_success C_LOCALE_OUTPUT 'git clean with absolute path' '
129130
would_clean=$(
130131
cd docs &&
131132
git clean -n "$(pwd)/../src" |
133+
grep part3 |
132134
sed -n -e "s|^Would remove ||p"
133135
) &&
134136
verbose test "$would_clean" = ../src/part3.c
@@ -547,7 +549,7 @@ test_expect_failure 'nested (non-empty) bare repositories should be cleaned even
547549
test_path_is_missing strange_bare
548550
'
549551

550-
test_expect_success 'giving path in nested git work tree will remove it' '
552+
test_expect_success 'giving path in nested git work tree will NOT remove it' '
551553
rm -fr repo &&
552554
mkdir repo &&
553555
(
@@ -559,7 +561,7 @@ test_expect_success 'giving path in nested git work tree will remove it' '
559561
git clean -f -d repo/bar/baz &&
560562
test_path_is_file repo/.git/HEAD &&
561563
test_path_is_dir repo/bar/ &&
562-
test_path_is_missing repo/bar/baz
564+
test_path_is_file repo/bar/baz/hello.world
563565
'
564566

565567
test_expect_success 'giving path to nested .git will not remove it' '
@@ -577,7 +579,7 @@ test_expect_success 'giving path to nested .git will not remove it' '
577579
test_path_is_dir untracked/
578580
'
579581

580-
test_expect_success 'giving path to nested .git/ will remove contents' '
582+
test_expect_success 'giving path to nested .git/ will NOT remove contents' '
581583
rm -fr repo untracked &&
582584
mkdir repo untracked &&
583585
(
@@ -587,7 +589,7 @@ test_expect_success 'giving path to nested .git/ will remove contents' '
587589
) &&
588590
git clean -f -d repo/.git/ &&
589591
test_path_is_dir repo/.git &&
590-
test_dir_is_empty repo/.git &&
592+
test_path_is_file repo/.git/HEAD &&
591593
test_path_is_dir untracked/
592594
'
593595

@@ -669,7 +671,7 @@ test_expect_success 'git clean -d skips untracked dirs containing ignored files'
669671
test_path_is_missing foo/b/bb
670672
'
671673

672-
test_expect_failure 'git clean -d skips nested repo containing ignored files' '
674+
test_expect_success 'git clean -d skips nested repo containing ignored files' '
673675
test_when_finished "rm -rf nested-repo-with-ignored-file" &&
674676
675677
git init nested-repo-with-ignored-file &&
@@ -691,6 +693,38 @@ test_expect_failure 'git clean -d skips nested repo containing ignored files' '
691693
test_path_is_file nested-repo-with-ignored-file/file
692694
'
693695

696+
test_expect_success 'git clean handles being told what to clean' '
697+
mkdir -p d1 d2 &&
698+
touch d1/ut d2/ut &&
699+
git clean -f */ut &&
700+
test_path_is_missing d1/ut &&
701+
test_path_is_missing d2/ut
702+
'
703+
704+
test_expect_success 'git clean handles being told what to clean, with -d' '
705+
mkdir -p d1 d2 &&
706+
touch d1/ut d2/ut &&
707+
git clean -ffd */ut &&
708+
test_path_is_missing d1/ut &&
709+
test_path_is_missing d2/ut
710+
'
711+
712+
test_expect_success 'git clean works if a glob is passed without -d' '
713+
mkdir -p d1 d2 &&
714+
touch d1/ut d2/ut &&
715+
git clean -f "*ut" &&
716+
test_path_is_missing d1/ut &&
717+
test_path_is_missing d2/ut
718+
'
719+
720+
test_expect_success 'git clean works if a glob is passed with -d' '
721+
mkdir -p d1 d2 &&
722+
touch d1/ut d2/ut &&
723+
git clean -ffd "*ut" &&
724+
test_path_is_missing d1/ut &&
725+
test_path_is_missing d2/ut
726+
'
727+
694728
test_expect_success MINGW 'handle clean & core.longpaths = false nicely' '
695729
test_config core.longpaths false &&
696730
a50=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &&

0 commit comments

Comments
 (0)