Skip to content

Commit da81d47

Browse files
committed
Merge branch 'en/keep-cwd'
Many git commands that deal with working tree files try to remove a directory that becomes empty (i.e. "git switch" from a branch that has the directory to another branch that does not would attempt remove all files in the directory and the directory itself). This drops users into an unfamiliar situation if the command was run in a subdirectory that becomes subject to removal due to the command. The commands have been taught to keep an empty directory if it is the directory they were started in to avoid surprising users. * en/keep-cwd: t2501: simplify the tests since we can now assume desired behavior dir: new flag to remove_dir_recurse() to spare the original_cwd dir: avoid incidentally removing the original_cwd in remove_path() stash: do not attempt to remove startup_info->original_cwd rebase: do not attempt to remove startup_info->original_cwd clean: do not attempt to remove startup_info->original_cwd symlinks: do not include startup_info->original_cwd in dir removal unpack-trees: add special cwd handling unpack-trees: refuse to remove startup_info->original_cwd setup: introduce startup_info->original_cwd t2501: add various tests for removing the current working directory
2 parents d0c99fc + 324b170 commit da81d47

File tree

13 files changed

+442
-22
lines changed

13 files changed

+442
-22
lines changed

builtin/clean.c

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_("Skipping repository %s\n");
3636
static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n");
3737
static const char *msg_warn_remove_failed = N_("failed to remove %s");
3838
static const char *msg_warn_lstat_failed = N_("could not lstat %s\n");
39+
static const char *msg_skip_cwd = N_("Refusing to remove current working directory\n");
40+
static const char *msg_would_skip_cwd = N_("Would refuse to remove current working directory\n");
3941

4042
enum color_clean {
4143
CLEAN_COLOR_RESET = 0,
@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
153155
{
154156
DIR *dir;
155157
struct strbuf quoted = STRBUF_INIT;
158+
struct strbuf realpath = STRBUF_INIT;
159+
struct strbuf real_ocwd = STRBUF_INIT;
156160
struct dirent *e;
157161
int res = 0, ret = 0, gone = 1, original_len = path->len, len;
158162
struct string_list dels = STRING_LIST_INIT_DUP;
@@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
231235
strbuf_setlen(path, original_len);
232236

233237
if (*dir_gone) {
234-
res = dry_run ? 0 : rmdir(path->buf);
235-
if (!res)
236-
*dir_gone = 1;
237-
else {
238-
int saved_errno = errno;
239-
quote_path(path->buf, prefix, &quoted, 0);
240-
errno = saved_errno;
241-
warning_errno(_(msg_warn_remove_failed), quoted.buf);
238+
/*
239+
* Normalize path components in path->buf, e.g. change '\' to
240+
* '/' on Windows.
241+
*/
242+
strbuf_realpath(&realpath, path->buf, 1);
243+
244+
/*
245+
* path and realpath are absolute; for comparison, we would
246+
* like to transform startup_info->original_cwd to an absolute
247+
* path too.
248+
*/
249+
if (startup_info->original_cwd)
250+
strbuf_realpath(&real_ocwd,
251+
startup_info->original_cwd, 1);
252+
253+
if (!strbuf_cmp(&realpath, &real_ocwd)) {
254+
printf("%s", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));
242255
*dir_gone = 0;
243-
ret = 1;
256+
} else {
257+
res = dry_run ? 0 : rmdir(path->buf);
258+
if (!res)
259+
*dir_gone = 1;
260+
else {
261+
int saved_errno = errno;
262+
quote_path(path->buf, prefix, &quoted, 0);
263+
errno = saved_errno;
264+
warning_errno(_(msg_warn_remove_failed), quoted.buf);
265+
*dir_gone = 0;
266+
ret = 1;
267+
}
244268
}
245269
}
246270

@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
250274
printf(dry_run ? _(msg_would_remove) : _(msg_remove), dels.items[i].string);
251275
}
252276
out:
277+
strbuf_release(&realpath);
278+
strbuf_release(&real_ocwd);
253279
strbuf_release(&quoted);
254280
string_list_clear(&dels, 0);
255281
return ret;

builtin/rm.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
399399
if (!index_only) {
400400
int removed = 0, gitmodules_modified = 0;
401401
struct strbuf buf = STRBUF_INIT;
402+
int flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;
402403
for (i = 0; i < list.nr; i++) {
403404
const char *path = list.entry[i].name;
404405
if (list.entry[i].is_submodule) {
405406
strbuf_reset(&buf);
406407
strbuf_addstr(&buf, path);
407-
if (remove_dir_recursively(&buf, 0))
408+
if (remove_dir_recursively(&buf, flag))
408409
die(_("could not remove '%s'"), path);
409410

410411
removed = 1;

builtin/stash.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,8 +1538,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
15381538
struct child_process cp = CHILD_PROCESS_INIT;
15391539

15401540
cp.git_cmd = 1;
1541+
if (startup_info->original_cwd)
1542+
cp.dir = startup_info->original_cwd;
15411543
strvec_pushl(&cp.args, "clean", "--force",
1542-
"--quiet", "-d", NULL);
1544+
"--quiet", "-d", ":/", NULL);
15431545
if (include_untracked == INCLUDE_ALL_FILES)
15441546
strvec_push(&cp.args, "-x");
15451547
if (run_command(&cp)) {

cache.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,8 +1846,10 @@ void overlay_tree_on_index(struct index_state *istate,
18461846
struct startup_info {
18471847
int have_repository;
18481848
const char *prefix;
1849+
const char *original_cwd;
18491850
};
18501851
extern struct startup_info *startup_info;
1852+
extern const char *tmp_original_cwd;
18511853

18521854
/* merge.c */
18531855
struct commit_list;

common-main.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)
2626
int main(int argc, const char **argv)
2727
{
2828
int result;
29+
struct strbuf tmp = STRBUF_INIT;
2930

3031
trace2_initialize_clock();
3132

@@ -49,6 +50,9 @@ int main(int argc, const char **argv)
4950
trace2_cmd_start(argv);
5051
trace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);
5152

53+
if (!strbuf_getcwd(&tmp))
54+
tmp_original_cwd = strbuf_detach(&tmp, NULL);
55+
5256
result = cmd_main(argc, argv);
5357

5458
/*

dir.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3160,6 +3160,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
31603160
int ret = 0, original_len = path->len, len, kept_down = 0;
31613161
int only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);
31623162
int keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);
3163+
int purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);
31633164
struct object_id submodule_head;
31643165

31653166
if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
@@ -3215,9 +3216,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
32153216
closedir(dir);
32163217

32173218
strbuf_setlen(path, original_len);
3218-
if (!ret && !keep_toplevel && !kept_down)
3219-
ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
3220-
else if (kept_up)
3219+
if (!ret && !keep_toplevel && !kept_down) {
3220+
if (!purge_original_cwd &&
3221+
startup_info->original_cwd &&
3222+
!strcmp(startup_info->original_cwd, path->buf))
3223+
ret = -1; /* Do not remove current working directory */
3224+
else
3225+
ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
3226+
} else if (kept_up)
32213227
/*
32223228
* report the uplevel that it is not an error that we
32233229
* did not rmdir() our directory.
@@ -3283,6 +3289,9 @@ int remove_path(const char *name)
32833289
slash = dirs + (slash - name);
32843290
do {
32853291
*slash = '\0';
3292+
if (startup_info->original_cwd &&
3293+
!strcmp(startup_info->original_cwd, dirs))
3294+
break;
32863295
} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));
32873296
free(dirs);
32883297
}

dir.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);
495495
/* Remove the contents of path, but leave path itself. */
496496
#define REMOVE_DIR_KEEP_TOPLEVEL 04
497497

498+
/* Remove the_original_cwd too */
499+
#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08
500+
498501
/*
499502
* Remove path and its contents, recursively. flags is a combination
500503
* of the above REMOVE_DIR_* constants. Return 0 on success.
@@ -504,7 +507,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);
504507
*/
505508
int remove_dir_recursively(struct strbuf *path, int flag);
506509

507-
/* tries to remove the path with empty directories along it, ignores ENOENT */
510+
/*
511+
* Tries to remove the path, along with leading empty directories so long as
512+
* those empty directories are not startup_info->original_cwd. Ignores
513+
* ENOENT.
514+
*/
508515
int remove_path(const char *path);
509516

510517
int fspathcmp(const char *a, const char *b);

sequencer.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4223,6 +4223,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,
42234223

42244224
cmd.git_cmd = 1;
42254225

4226+
if (startup_info->original_cwd)
4227+
cmd.dir = startup_info->original_cwd;
42264228
strvec_push(&cmd.args, "checkout");
42274229
strvec_push(&cmd.args, commit);
42284230
strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION "=%s", action);

setup.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ static int work_tree_config_is_bogus;
1212

1313
static struct startup_info the_startup_info;
1414
struct startup_info *startup_info = &the_startup_info;
15+
const char *tmp_original_cwd;
1516

1617
/*
1718
* The input parameter must contain an absolute path, and it must already be
@@ -432,6 +433,69 @@ void setup_work_tree(void)
432433
initialized = 1;
433434
}
434435

436+
static void setup_original_cwd(void)
437+
{
438+
struct strbuf tmp = STRBUF_INIT;
439+
const char *worktree = NULL;
440+
int offset = -1;
441+
442+
if (!tmp_original_cwd)
443+
return;
444+
445+
/*
446+
* startup_info->original_cwd points to the current working
447+
* directory we inherited from our parent process, which is a
448+
* directory we want to avoid removing.
449+
*
450+
* For convience, we would like to have the path relative to the
451+
* worktree instead of an absolute path.
452+
*
453+
* Yes, startup_info->original_cwd is usually the same as 'prefix',
454+
* but differs in two ways:
455+
* - prefix has a trailing '/'
456+
* - if the user passes '-C' to git, that modifies the prefix but
457+
* not startup_info->original_cwd.
458+
*/
459+
460+
/* Normalize the directory */
461+
strbuf_realpath(&tmp, tmp_original_cwd, 1);
462+
free((char*)tmp_original_cwd);
463+
tmp_original_cwd = NULL;
464+
startup_info->original_cwd = strbuf_detach(&tmp, NULL);
465+
466+
/*
467+
* Get our worktree; we only protect the current working directory
468+
* if it's in the worktree.
469+
*/
470+
worktree = get_git_work_tree();
471+
if (!worktree)
472+
goto no_prevention_needed;
473+
474+
offset = dir_inside_of(startup_info->original_cwd, worktree);
475+
if (offset >= 0) {
476+
/*
477+
* If startup_info->original_cwd == worktree, that is already
478+
* protected and we don't need original_cwd as a secondary
479+
* protection measure.
480+
*/
481+
if (!*(startup_info->original_cwd + offset))
482+
goto no_prevention_needed;
483+
484+
/*
485+
* original_cwd was inside worktree; precompose it just as
486+
* we do prefix so that built up paths will match
487+
*/
488+
startup_info->original_cwd = \
489+
precompose_string_if_needed(startup_info->original_cwd
490+
+ offset);
491+
return;
492+
}
493+
494+
no_prevention_needed:
495+
free((char*)startup_info->original_cwd);
496+
startup_info->original_cwd = NULL;
497+
}
498+
435499
static int read_worktree_config(const char *var, const char *value, void *vdata)
436500
{
437501
struct repository_format *data = vdata;
@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
13301394
setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
13311395
}
13321396

1397+
setup_original_cwd();
13331398

13341399
strbuf_release(&dir);
13351400
strbuf_release(&gitdir);

symlinks.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)
279279
{
280280
while (removal.len > new_len) {
281281
removal.buf[removal.len] = '\0';
282-
if (rmdir(removal.buf))
282+
if ((startup_info->original_cwd &&
283+
!strcmp(removal.buf, startup_info->original_cwd)) ||
284+
rmdir(removal.buf))
283285
break;
284286
do {
285287
removal.len--;
@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)
293295
{
294296
int match_len, last_slash, i, previous_slash;
295297

298+
if (startup_info->original_cwd &&
299+
!strcmp(name, startup_info->original_cwd))
300+
return; /* Do not remove the current working directory */
301+
296302
match_len = last_slash = i =
297303
longest_path_match(name, len, removal.buf, removal.len,
298304
&previous_slash);

0 commit comments

Comments
 (0)