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
This commit is contained in:
commit
aafb75452b
@ -26,18 +26,20 @@ are affected.
|
||||
OPTIONS
|
||||
-------
|
||||
-d::
|
||||
Remove untracked directories in addition to untracked files.
|
||||
If an untracked directory is managed by a different Git
|
||||
repository, it is not removed by default. Use -f option twice
|
||||
if you really want to remove such a directory.
|
||||
Normally, when no <path> is specified, git clean will not
|
||||
recurse into untracked directories to avoid removing too much.
|
||||
Specify -d to have it recurse into such directories as well.
|
||||
If any paths are specified, -d is irrelevant; all untracked
|
||||
files matching the specified paths (with exceptions for nested
|
||||
git directories mentioned under `--force`) will be removed.
|
||||
|
||||
-f::
|
||||
--force::
|
||||
If the Git configuration variable clean.requireForce is not set
|
||||
to false, 'git clean' will refuse to delete files or directories
|
||||
unless given -f, -n or -i. Git will refuse to delete directories
|
||||
with .git sub directory or file unless a second -f
|
||||
is given.
|
||||
unless given -f or -i. Git will refuse to modify untracked
|
||||
nested git repositories (directories with a .git subdirectory)
|
||||
unless a second -f is given.
|
||||
|
||||
-i::
|
||||
--interactive::
|
||||
|
@ -158,7 +158,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
|
||||
|
||||
*dir_gone = 1;
|
||||
|
||||
if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) && is_nonbare_repository_dir(path)) {
|
||||
if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
|
||||
is_nonbare_repository_dir(path)) {
|
||||
if (!quiet) {
|
||||
quote_path_relative(path->buf, prefix, "ed);
|
||||
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)
|
||||
|
||||
if (force > 1)
|
||||
rm_flags = 0;
|
||||
else
|
||||
dir.flags |= DIR_SKIP_NESTED_GIT;
|
||||
|
||||
dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
|
||||
|
||||
if (argc) {
|
||||
/*
|
||||
* Remaining args implies pathspecs specified, and we should
|
||||
* recurse within those.
|
||||
*/
|
||||
remove_directories = 1;
|
||||
}
|
||||
|
||||
if (remove_directories)
|
||||
dir.flags |= DIR_SHOW_IGNORED_TOO | DIR_KEEP_UNTRACKED_CONTENTS;
|
||||
|
||||
@ -1007,6 +1018,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
|
||||
for_each_string_list_item(item, &del_list) {
|
||||
struct stat st;
|
||||
|
||||
strbuf_reset(&abs_path);
|
||||
if (prefix)
|
||||
strbuf_addstr(&abs_path, prefix);
|
||||
|
||||
@ -1040,7 +1052,6 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
|
||||
printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
|
||||
}
|
||||
}
|
||||
strbuf_reset(&abs_path);
|
||||
}
|
||||
|
||||
strbuf_release(&abs_path);
|
||||
|
65
dir.c
65
dir.c
@ -139,7 +139,7 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
|
||||
* ":(icase)path" is treated as a pathspec full of
|
||||
* wildcard. In other words, only prefix is considered common
|
||||
* prefix. If the pathspec is abc/foo abc/bar, running in
|
||||
* subdir xyz, the common prefix is still xyz, not xuz/abc as
|
||||
* subdir xyz, the common prefix is still xyz, not xyz/abc as
|
||||
* in non-:(icase).
|
||||
*/
|
||||
GUARD_PATHSPEC(pathspec,
|
||||
@ -273,19 +273,30 @@ static int do_read_blob(const struct object_id *oid, struct oid_stat *oid_stat,
|
||||
|
||||
#define DO_MATCH_EXCLUDE (1<<0)
|
||||
#define DO_MATCH_DIRECTORY (1<<1)
|
||||
#define DO_MATCH_SUBMODULE (1<<2)
|
||||
#define DO_MATCH_LEADING_PATHSPEC (1<<2)
|
||||
|
||||
/*
|
||||
* Does 'match' match the given name?
|
||||
* A match is found if
|
||||
* Does the given pathspec match the given name? A match is found if
|
||||
*
|
||||
* (1) the 'match' string is leading directory of 'name', or
|
||||
* (2) the 'match' string is a wildcard and matches 'name', or
|
||||
* (3) the 'match' string is exactly the same as 'name'.
|
||||
* (1) the pathspec string is leading directory of 'name' ("RECURSIVELY"), or
|
||||
* (2) the pathspec string has a leading part matching 'name' ("LEADING"), or
|
||||
* (3) the pathspec string is a wildcard and matches 'name' ("WILDCARD"), or
|
||||
* (4) the pathspec string is exactly the same as 'name' ("EXACT").
|
||||
*
|
||||
* and the return value tells which case it was.
|
||||
* Return value tells which case it was (1-4), or 0 when there is no match.
|
||||
*
|
||||
* It returns 0 when there is no match.
|
||||
* It may be instructive to look at a small table of concrete examples
|
||||
* to understand the differences between 1, 2, and 4:
|
||||
*
|
||||
* Pathspecs
|
||||
* | a/b | a/b/ | a/b/c
|
||||
* ------+-----------+-----------+------------
|
||||
* a/b | EXACT | EXACT[1] | LEADING[2]
|
||||
* Names a/b/ | RECURSIVE | EXACT | LEADING[2]
|
||||
* a/b/c | RECURSIVE | RECURSIVE | EXACT
|
||||
*
|
||||
* [1] Only if DO_MATCH_DIRECTORY is passed; otherwise, this is NOT a match.
|
||||
* [2] Only if DO_MATCH_LEADING_PATHSPEC is passed; otherwise, not a match.
|
||||
*/
|
||||
static int match_pathspec_item(const struct index_state *istate,
|
||||
const struct pathspec_item *item, int prefix,
|
||||
@ -353,13 +364,14 @@ static int match_pathspec_item(const struct index_state *istate,
|
||||
item->nowildcard_len - prefix))
|
||||
return MATCHED_FNMATCH;
|
||||
|
||||
/* Perform checks to see if "name" is a super set of the pathspec */
|
||||
if (flags & DO_MATCH_SUBMODULE) {
|
||||
/* Perform checks to see if "name" is a leading string of the pathspec */
|
||||
if (flags & DO_MATCH_LEADING_PATHSPEC) {
|
||||
/* name is a literal prefix of the pathspec */
|
||||
int offset = name[namelen-1] == '/' ? 1 : 0;
|
||||
if ((namelen < matchlen) &&
|
||||
(match[namelen] == '/') &&
|
||||
(match[namelen-offset] == '/') &&
|
||||
!ps_strncmp(item, match, name, namelen))
|
||||
return MATCHED_RECURSIVELY;
|
||||
return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
|
||||
|
||||
/* name" doesn't match up to the first wild character */
|
||||
if (item->nowildcard_len < item->len &&
|
||||
@ -376,7 +388,7 @@ static int match_pathspec_item(const struct index_state *istate,
|
||||
* The submodules themselves will be able to perform more
|
||||
* accurate matching to determine if the pathspec matches.
|
||||
*/
|
||||
return MATCHED_RECURSIVELY;
|
||||
return MATCHED_RECURSIVELY_LEADING_PATHSPEC;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -497,7 +509,7 @@ int submodule_path_match(const struct index_state *istate,
|
||||
strlen(submodule_name),
|
||||
0, seen,
|
||||
DO_MATCH_DIRECTORY |
|
||||
DO_MATCH_SUBMODULE);
|
||||
DO_MATCH_LEADING_PATHSPEC);
|
||||
return matched;
|
||||
}
|
||||
|
||||
@ -1451,6 +1463,16 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
|
||||
return path_none;
|
||||
|
||||
case index_nonexistent:
|
||||
if (dir->flags & DIR_SKIP_NESTED_GIT) {
|
||||
int nested_repo;
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
strbuf_addstr(&sb, dirname);
|
||||
nested_repo = is_nonbare_repository_dir(&sb);
|
||||
strbuf_release(&sb);
|
||||
if (nested_repo)
|
||||
return path_none;
|
||||
}
|
||||
|
||||
if (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)
|
||||
break;
|
||||
if (exclude &&
|
||||
@ -1950,8 +1972,11 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
|
||||
/* recurse into subdir if instructed by treat_path */
|
||||
if ((state == path_recurse) ||
|
||||
((state == path_untracked) &&
|
||||
(dir->flags & DIR_SHOW_IGNORED_TOO) &&
|
||||
(get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR))) {
|
||||
(get_dtype(cdir.de, istate, path.buf, path.len) == DT_DIR) &&
|
||||
((dir->flags & DIR_SHOW_IGNORED_TOO) ||
|
||||
(pathspec &&
|
||||
do_match_pathspec(istate, pathspec, path.buf, path.len,
|
||||
baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)))) {
|
||||
struct untracked_cache_dir *ud;
|
||||
ud = lookup_untracked(dir->untracked, untracked,
|
||||
path.buf + baselen,
|
||||
@ -1962,6 +1987,12 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
|
||||
check_only, stop_at_first_file, pathspec);
|
||||
if (subdir_state > dir_state)
|
||||
dir_state = subdir_state;
|
||||
|
||||
if (pathspec &&
|
||||
!match_pathspec(istate, pathspec, path.buf, path.len,
|
||||
0 /* prefix */, NULL,
|
||||
0 /* do NOT special case dirs */))
|
||||
state = path_none;
|
||||
}
|
||||
|
||||
if (check_only) {
|
||||
|
8
dir.h
8
dir.h
@ -156,7 +156,8 @@ struct dir_struct {
|
||||
DIR_SHOW_IGNORED_TOO = 1<<5,
|
||||
DIR_COLLECT_KILLED_ONLY = 1<<6,
|
||||
DIR_KEEP_UNTRACKED_CONTENTS = 1<<7,
|
||||
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8
|
||||
DIR_SHOW_IGNORED_TOO_MODE_MATCHING = 1<<8,
|
||||
DIR_SKIP_NESTED_GIT = 1<<9
|
||||
} flags;
|
||||
struct dir_entry **entries;
|
||||
struct dir_entry **ignored;
|
||||
@ -211,8 +212,9 @@ int count_slashes(const char *s);
|
||||
* when populating the seen[] array.
|
||||
*/
|
||||
#define MATCHED_RECURSIVELY 1
|
||||
#define MATCHED_FNMATCH 2
|
||||
#define MATCHED_EXACTLY 3
|
||||
#define MATCHED_RECURSIVELY_LEADING_PATHSPEC 2
|
||||
#define MATCHED_FNMATCH 3
|
||||
#define MATCHED_EXACTLY 4
|
||||
int simple_length(const char *match);
|
||||
int no_wildcard(const char *string);
|
||||
char *common_prefix(const struct pathspec *pathspec);
|
||||
|
@ -131,4 +131,24 @@ $test_unicode 'merge (silent unicode normalization)' '
|
||||
git merge topic
|
||||
'
|
||||
|
||||
test_expect_success CASE_INSENSITIVE_FS 'checkout with no pathspec and a case insensitive fs' '
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
>Gitweb &&
|
||||
git add Gitweb &&
|
||||
git commit -m "add Gitweb" &&
|
||||
|
||||
git checkout --orphan todo &&
|
||||
git reset --hard &&
|
||||
mkdir -p gitweb/subdir &&
|
||||
>gitweb/subdir/file &&
|
||||
git add gitweb &&
|
||||
git commit -m "add gitweb/subdir/file" &&
|
||||
|
||||
git checkout master
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
|
@ -117,6 +117,7 @@ test_expect_success C_LOCALE_OUTPUT 'git clean with relative prefix' '
|
||||
would_clean=$(
|
||||
cd docs &&
|
||||
git clean -n ../src |
|
||||
grep part3 |
|
||||
sed -n -e "s|^Would remove ||p"
|
||||
) &&
|
||||
verbose test "$would_clean" = ../src/part3.c
|
||||
@ -129,6 +130,7 @@ test_expect_success C_LOCALE_OUTPUT 'git clean with absolute path' '
|
||||
would_clean=$(
|
||||
cd docs &&
|
||||
git clean -n "$(pwd)/../src" |
|
||||
grep part3 |
|
||||
sed -n -e "s|^Would remove ||p"
|
||||
) &&
|
||||
verbose test "$would_clean" = ../src/part3.c
|
||||
@ -547,7 +549,7 @@ test_expect_failure 'nested (non-empty) bare repositories should be cleaned even
|
||||
test_path_is_missing strange_bare
|
||||
'
|
||||
|
||||
test_expect_success 'giving path in nested git work tree will remove it' '
|
||||
test_expect_success 'giving path in nested git work tree will NOT remove it' '
|
||||
rm -fr repo &&
|
||||
mkdir repo &&
|
||||
(
|
||||
@ -559,7 +561,7 @@ test_expect_success 'giving path in nested git work tree will remove it' '
|
||||
git clean -f -d repo/bar/baz &&
|
||||
test_path_is_file repo/.git/HEAD &&
|
||||
test_path_is_dir repo/bar/ &&
|
||||
test_path_is_missing repo/bar/baz
|
||||
test_path_is_file repo/bar/baz/hello.world
|
||||
'
|
||||
|
||||
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' '
|
||||
test_path_is_dir untracked/
|
||||
'
|
||||
|
||||
test_expect_success 'giving path to nested .git/ will remove contents' '
|
||||
test_expect_success 'giving path to nested .git/ will NOT remove contents' '
|
||||
rm -fr repo untracked &&
|
||||
mkdir repo untracked &&
|
||||
(
|
||||
@ -587,7 +589,7 @@ test_expect_success 'giving path to nested .git/ will remove contents' '
|
||||
) &&
|
||||
git clean -f -d repo/.git/ &&
|
||||
test_path_is_dir repo/.git &&
|
||||
test_dir_is_empty repo/.git &&
|
||||
test_path_is_file repo/.git/HEAD &&
|
||||
test_path_is_dir untracked/
|
||||
'
|
||||
|
||||
@ -669,7 +671,7 @@ test_expect_success 'git clean -d skips untracked dirs containing ignored files'
|
||||
test_path_is_missing foo/b/bb
|
||||
'
|
||||
|
||||
test_expect_failure 'git clean -d skips nested repo containing ignored files' '
|
||||
test_expect_success 'git clean -d skips nested repo containing ignored files' '
|
||||
test_when_finished "rm -rf nested-repo-with-ignored-file" &&
|
||||
|
||||
git init nested-repo-with-ignored-file &&
|
||||
@ -691,6 +693,38 @@ test_expect_failure 'git clean -d skips nested repo containing ignored files' '
|
||||
test_path_is_file nested-repo-with-ignored-file/file
|
||||
'
|
||||
|
||||
test_expect_success 'git clean handles being told what to clean' '
|
||||
mkdir -p d1 d2 &&
|
||||
touch d1/ut d2/ut &&
|
||||
git clean -f */ut &&
|
||||
test_path_is_missing d1/ut &&
|
||||
test_path_is_missing d2/ut
|
||||
'
|
||||
|
||||
test_expect_success 'git clean handles being told what to clean, with -d' '
|
||||
mkdir -p d1 d2 &&
|
||||
touch d1/ut d2/ut &&
|
||||
git clean -ffd */ut &&
|
||||
test_path_is_missing d1/ut &&
|
||||
test_path_is_missing d2/ut
|
||||
'
|
||||
|
||||
test_expect_success 'git clean works if a glob is passed without -d' '
|
||||
mkdir -p d1 d2 &&
|
||||
touch d1/ut d2/ut &&
|
||||
git clean -f "*ut" &&
|
||||
test_path_is_missing d1/ut &&
|
||||
test_path_is_missing d2/ut
|
||||
'
|
||||
|
||||
test_expect_success 'git clean works if a glob is passed with -d' '
|
||||
mkdir -p d1 d2 &&
|
||||
touch d1/ut d2/ut &&
|
||||
git clean -ffd "*ut" &&
|
||||
test_path_is_missing d1/ut &&
|
||||
test_path_is_missing d2/ut
|
||||
'
|
||||
|
||||
test_expect_success MINGW 'handle clean & core.longpaths = false nicely' '
|
||||
test_config core.longpaths false &&
|
||||
a50=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &&
|
||||
|
Loading…
Reference in New Issue
Block a user