Avoid removing the current working directory, even if it becomes empty
Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
In reply to Eric SunshineQuoted 19 lines · Eric Sunshine
Quoted 9 lines · Phillip Wood
Quoted 8 lines · Elijah Newren
There's nothing wrong with running checkout from a subdirectory. It is unfortunate that
setup.cauto-discovers both the git directory and the working tree, but setsGIT_DIRwithout settingGIT_WORK_TREEin the case of a non-main worktree; it's not particularly friendly for subcommands. Of course, it's also unfortunate that sequencer still forks subprocesses other than those requested by a user with e.g. --exec.Quoted 7 lines
Quoted 2 lines
But, anyway, I've got a patch that I'll send as soon as it passes CI
(https://github.com/git/git/pull/1205).The patch hasn't come through to me on the mailing list yet, but it looks good. I thought we set both
GIT_DIRandGIT_WORK_TREEwhen we were in a non-main worktree but obviously we don't. Eric do you happen to know if that is intentional?
Quoted 5 lines
As far as I know, there is no reason to set
GIT_DIRandGIT_WORK_TREE, in general, when in a linked worktree since each worktree has its own .git file ("gitfile") which tells Git commands where the repository is and signals that that directory itself (which contains the .git file) is indeed a Git worktree.
Oh, interesting. Not setting GIT_DIR either does sound a bit better.
...though after digging for a while, it turns out to be a bit more involved than I thought. Although the below patch passes our current testsuite and fixes the reported bug, I'm worried I've missed some cases not tested by the testsuite.
Not sure if we want to pursue this, drop it, or something else. Thoughts?
-- >8 --
Subject: [RFC/POC PATCH] setup: do not pre-emptively set GIT_DIR based on discovery
setup_git_directory_gently() handles a few cases, but when it discovers the git directory and finds it is not ".git", it sets and exports the GIT_DIR environment variable to the discovered value. It does not set GIT_WORK_TREE, though, even when it also discovers it. This has two drawbacks:
* Since Git assumes the working tree is '.' when GIT_DIR is set and GIT_WORK_TREE isn't, any subprocesses called by Git which need to operate on the working tree either need to be careful to always invoke with their cwd being the toplevel of the actual working tree (and potentially losing information about 'prefix'), or take care to also specify the GIT_WORK_TREE environment value when forking.
* The GIT_DIR environment variable behaves somewhat like an implicit 'the_repository'; it's another bit of global state that is potentially problematic for libifying efforts of libgit.
Avoid these issues by no longer exporting GIT_DIR from the setup code (other locations like the parser handling the --git-dir flag will still set it, though).
Some comments on the various code changes:
* clone/push/fetch related:
* there are *many* subprocesses involved in fetch/push and friends, and they typically need to know the GIT_DIR they are operating on
* this involves: fetch-patch.c, connected.c, bundle.c, clone.c, transport-helper.c, receive-pack.c, upload-pack.c
* this accounts for the majority of this patch
* much of this work could be avoided by having enter_repo() call xsetenv(GIT_DIR_ENVIRONMENT, ".", 1) just after its set_git_dir() call, but I don't know if that'd be considered a half measure
* rev-parse.c: substitute usage of the GIT_DIR environment variable with the_repository->gitdir that should have the same value (exept when the latter is ".git" the former will be NULL, so there are some small logic tweaks).
* run-command: hooks have been operating under the assumption that GIT_DIR is set for a long time. This is possibly counter-productive for the same reasons listed above, and it has certainly been brought up before on the list, but backward compatibility concerns led me to just propagate it to avoid breaking anything.
* test-subprocess: not sure if we should automatically export GIT_DIR, but the existing code and one test in the repo relied on it, so I just included it.
--- builtin/clone.c | 7 ++++++- builtin/receive-pack.c | 12 ++++++++++++ builtin/rev-parse.c | 8 +++----- bundle.c | 2 ++ connected.c | 2 ++ environment.c | 1 - fetch-pack.c | 6 ++++++ run-command.c | 2 ++ t/helper/test-subprocess.c | 2 ++ transport-helper.c | 3 +++ upload-pack.c | 2 ++ 11 files changed, 40 insertions(+), 7 deletions(-)
builtin/clone.c +6 -1
@@ -836,13 +836,18 @@ static void dissociate_from_references(void) {static const char * argv[] = {"repack" ,"-a" ,"-d" ,NULL };char *alternates =git_pathdup ("objects/info/alternates" ); +struct strvec env =STRVEC_INIT ; +strvec_pushf (&env,GIT_DIR_ENVIRONMENT "=%s" , the_repository->gitdir);if (!access (alternates,F_OK )) { -if (run_command_v_opt (argv,RUN_GIT_CMD |RUN_COMMAND_NO_STDIN )) +if (run_command_v_opt_cd_env (argv, +RUN_GIT_CMD |RUN_COMMAND_NO_STDIN , +NULL , env.v))die (_ ("cannot repack to clean up" ));if (unlink (alternates) && errno !=ENOENT )die_errno (_ ("cannot unlink temporary alternates file" )); } +strvec_clear (&env);free (alternates); }
builtin/receive-pack.c +12 -0
@@ -825,6 +825,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, proc.stdout_to_stderr =1 ; proc.trace2_hook_name = hook_name; +strvec_pushf (&proc.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (feed_state->push_options) {int i;for (i =0 ; i < feed_state->push_options->nr; i++)
@@ -953,6 +955,8 @@ static int run_update_hook(struct command *cmd)strvec_push (&proc.args, cmd->ref_name);strvec_push (&proc.args,oid_to_hex (&cmd->old_oid));strvec_push (&proc.args,oid_to_hex (&cmd->new_oid)); +strvec_pushf (&proc.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir); proc.no_stdin =1 ; proc.stdout_to_stderr =1 ;
@@ -1128,6 +1132,8 @@ static int run_proc_receive_hook(struct command *commands, }strvec_push (&proc.args, hook_path); +strvec_pushf (&proc.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir); proc.in =-1 ; proc.out =-1 ; proc.trace2_hook_name ="proc-receive" ;
@@ -1645,6 +1651,8 @@ static void run_update_post_hook(struct command *commands) proc.stdout_to_stderr =1 ; proc.err = use_sideband ?-1 :0 ; proc.trace2_hook_name ="post-update" ; +strvec_pushf (&proc.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (!start_command (&proc)) {if (use_sideband)
@@ -2236,6 +2244,8 @@ static const char *unpack(int err_fd, struct shallow_info *si) child.no_stdout =1 ; child.err = err_fd; child.git_cmd =1 ; +strvec_pushf (&child.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir); status =run_command (&child);if (status)return "unpack-objects abnormal exit" ;
@@ -2267,6 +2277,8 @@ static const char *unpack(int err_fd, struct shallow_info *si) child.out =-1 ; child.err = err_fd; child.git_cmd =1 ; +strvec_pushf (&child.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir); status =start_command (&child);if (status)return "index-pack fork failed" ;
builtin/rev-parse.c +3 -5
@@ -930,12 +930,12 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) }if (!strcmp (arg,"--git-dir" ) || !strcmp (arg,"--absolute-git-dir" )) { -const char *gitdir =getenv (GIT_DIR_ENVIRONMENT ); +const char *gitdir = the_repository->gitdir;char *cwd;int len;enum format_type wanted = format;if (arg[2 ] =='g' ) {/* --git-dir */ -if (gitdir) { +if (strcmp (gitdir,".git" )) {print_path (gitdir, prefix, format,DEFAULT_UNMODIFIED );continue ; }
@@ -945,9 +945,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) } }else {/* --absolute-git-dir */ wanted =FORMAT_CANONICAL ; -if (!gitdir && !prefix) - gitdir =".git" ; -if (gitdir) { +if (strcmp (gitdir,".git" ) || !prefix) {struct strbuf realpath =STRBUF_INIT ;strbuf_realpath (&realpath, gitdir,1 );puts (realpath.buf);
bundle.c +2 -0
@@ -573,6 +573,8 @@ int unbundle(struct repository *r, struct bundle_header *header, {struct child_process ip =CHILD_PROCESS_INIT ;strvec_pushl (&ip.args,"index-pack" ,"--fix-thin" ,"--stdin" ,NULL ); +strvec_pushf (&ip.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (extra_index_pack_args) {strvec_pushv (&ip.args, extra_index_pack_args->v);
connected.c +2 -0
@@ -109,6 +109,8 @@ int check_connected(oid_iterate_fn fn, void *cb_data, _("Checking connectivity")); rev_list.git_cmd =1 ; +strvec_pushf (&rev_list.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (opt->env)strvec_pushv (&rev_list.env_array, opt->env); rev_list.in =-1 ;
environment.c +0 -1
@@ -327,7 +327,6 @@ char *get_graft_file(struct repository *r)static void set_git_dir_1 (const char *path) { -xsetenv (GIT_DIR_ENVIRONMENT , path,1 );setup_git_env (path); }
fetch-pack.c +6 -0
@@ -892,6 +892,8 @@ static int get_pack(struct fetch_pack_args *args, cmd_name ="index-pack" ;strvec_push (&cmd.args, cmd_name);strvec_push (&cmd.args,"--stdin" ); +strvec_pushf (&cmd.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (!args->quiet && !args->no_progress)strvec_push (&cmd.args,"-v" );if (args->use_thin_pack)
@@ -927,6 +929,8 @@ static int get_pack(struct fetch_pack_args *args, }else { cmd_name ="unpack-objects" ; +strvec_pushf (&cmd.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);strvec_push (&cmd.args, cmd_name);if (args->quiet || args->no_progress)strvec_push (&cmd.args,"-q" );
@@ -1703,6 +1707,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,strvec_pushf (&cmd.args,"--index-pack-arg=%s" , index_pack_args.v[j]);strvec_push (&cmd.args, uri); +strvec_pushf (&cmd.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir); cmd.git_cmd =1 ; cmd.no_stdin =1 ; cmd.out =-1 ;
run-command.c +2 -0
@@ -1319,6 +1319,8 @@ int run_hook_ve(const char *const *env, const char *name, va_list args)strvec_push (&hook.args, p); while ((p = va_arg(args, const char *)))strvec_push (&hook.args, p); +strvec_pushf (&hook.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);if (env)strvec_pushv (&hook.env_array, (const char **)env); hook.no_stdin =1 ;
t/helper/test-subprocess.c +2 -0
@@ -16,5 +16,7 @@ int cmd__subprocess(int argc, const char **argv) } cp.git_cmd =1 ;strvec_pushv (&cp.args, (const char **)argv +1 ); +strvec_pushf (&cp.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);return run_command (&cp); }
transport-helper.c +3 -0
@@ -448,6 +448,9 @@ static int get_importer(struct transport *transport, struct child_process *fastistrvec_pushf (&fastimport->args,"--cat-blob-fd=%d" , cat_blob_fd); } fastimport->git_cmd =1 ; +if (have_git_dir ()) +strvec_pushf (&fastimport->env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT ,get_git_dir ()); code =start_command (fastimport);return code;
upload-pack.c +2 -0
@@ -295,6 +295,8 @@ static void create_pack_file(struct upload_pack_data *pack_data,strvec_push (&pack_objects.args,"--shallow-file" );strvec_push (&pack_objects.args,"" ); } +strvec_pushf (&pack_objects.env_array,"%s=%s" , +GIT_DIR_ENVIRONMENT , the_repository->gitdir);strvec_push (&pack_objects.args,"pack-objects" );strvec_push (&pack_objects.args,"--revs" );if (pack_data->use_thin_pack)
Signature · 1 line
2.35.0.2.g9420af4777.dirty
Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
Quoted 7 lines · Elijah Newren
Quoted 5 lines · Eric Sunshine
As far as I know, there is no reason to set
GIT_DIRandGIT_WORK_TREE, in general, when in a linked worktree since each worktree has its own .git file ("gitfile") which tells Git commands where the repository is and signals that that directory itself (which contains the .git file) is indeed a Git worktree.
Quoted 8 lines
Oh, interesting. Not setting
GIT_DIReither does sound a bit better....though after digging for a while, it turns out to be a bit more involved than I thought. Although the below patch passes our current testsuite and fixes the reported bug, I'm worried I've missed some cases not tested by the testsuite.
Not sure if we want to pursue this, drop it, or something else. Thoughts?
It's an enticing idea, though I have no deep knowledge about all the possible interactions which may be impacted by such a change. Duy had a deep understanding of how all this worked, and probably Peff, as well, but they aren't around to offer opinions.
set_git_dir() has been setting the GIT_DIR environment variable ever since it (the function) was introduced by d7ac12b25d (Add set_git_dir() function, 2007-08-01). Unfortunately, the commit message doesn't explain why it does so.
More below...
Quoted 13 lines
-- >8 --
Subject: [RFC/POC PATCH] setup: do not pre-emptively setGIT_DIRbased on discoverySome comments on the various code changes:
* clone/push/fetch related:
* there are *many* subprocesses involved in fetch/push and friends, and they typically need to know theGIT_DIRthey are operating on
* this involves:fetch-patch.c,connected.c,bundle.c,clone.c,transport-helper.c,receive-pack.c,upload-pack.c
* this accounts for the majority of this patch
* much of this work could be avoided by havingenter_repo()callxsetenv(GIT_DIR_ENVIRONMENT, ".", 1)just after itsset_git_dir()call, but I don't know if that'd be considered a half measure
It does feel a bit like a bandaid to insert new code at these locations to set GIT_DIR manually. It's not clear to readers why GIT_DIR is needed for these specific cases, nor what the impact is when it is not set. Thus, one wonders if such a blanket approach is indeed required or if a more narrow and directed fix can be applied, such as calling subprograms with an explicit --git-dir= rather than setting GIT_DIR with its potentially more broad and difficult-to-reason-about impact.
builtin/clone.c static void dissociate_from_references(void) · line 838
Quoted 7 lines
Signed-off-by Elijah Newren <[email protected]>---
builtin/clone.c +2
-0@@ -836,13 +836,18 @@ static void dissociate_from_references(void) { +struct strvec env =STRVEC_INIT ; +strvec_pushf (&env,GIT_DIR_ENVIRONMENT "=%s" , the_repository->gitdir);
Minor inconsistency: all the other similar changes in this patch use "%s=%s" and then pass in GIT_DIR_ENVIRONMENT to be interpolated by %s.
builtin/rev-parse.c int cmd_rev_parse(int argc, const char **argv, const char *prefix) · line 945
Quoted 12 lines
builtin/rev-parse.c +3
-5@@ -930,12 +930,12 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) -const char *gitdir =getenv (GIT_DIR_ENVIRONMENT ); +const char *gitdir = the_repository->gitdir;if (arg[2 ] =='g' ) {/* --git-dir */ -if (gitdir) { +if (strcmp (gitdir,".git" )) {@@ -945,9 +945,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) -if (!gitdir && !prefix) - gitdir =".git" ; -if (gitdir) { +if (strcmp (gitdir,".git" ) || !prefix) {
The meaning here becomes more obscure with this change applied. In the original code, it was obvious enough that non-NULL gitdir meant that the GIT_DIR environment variable had a value, but strcmp(gitdir,".git") probably doesn't convey much to readers of this code? Assigning the result of the strcmp() to a well-named variable could go a long way toward making the meaning clearer. Or, an in-code comment might be warranted.
Quoted 7 lines
environment.c +0
-1@@ -327,7 +327,6 @@ char *get_graft_file(struct repository *r)static void set_git_dir_1 (const char *path) { -xsetenv (GIT_DIR_ENVIRONMENT , path,1 );setup_git_env (path); }
Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
Quoted 9 lines · Eric Sunshine
Quoted 7 lines · Elijah Newren
Some comments on the various code changes:
* clone/push/fetch related:
* there are *many* subprocesses involved in fetch/push and friends, and they typically need to know theGIT_DIRthey are operating on
* this involves:fetch-patch.c,connected.c,bundle.c,clone.c,transport-helper.c,receive-pack.c,upload-pack.c
* this accounts for the majority of this patch
Quoted 8 lines
It does feel a bit like a bandaid to insert new code at these locations to set
GIT_DIRmanually. It's not clear to readers whyGIT_DIRis needed for these specific cases, nor what the impact is when it is not set. Thus, one wonders if such a blanket approach is indeed required or if a more narrow and directed fix can be applied, such as calling subprograms with an explicit --git-dir= rather than settingGIT_DIRwith its potentially more broad and difficult-to-reason-about impact.
I meant to ask here what was the nature of the various failures you were seeing without GIT_DIR being set, and whether you had considered tackling those failures with --git-dir= instead of GIT_DIR. If so, were the problems too difficult to overcome by --git-dir= alone? Fleshing out the commit message with such information might be worthwhile.
By the way, I also didn't mean to imply that the "difficult-to-reason-about GIT_DIR interaction in relation to subprograms" problem is new to this patch; it isn't. That problem has been around for ages (at least since 2007-08-01) but it wasn't as obvious since the setting of GIT_DIR was so far removed from other code which runs subprograms, thus readers likely wouldn't be thinking about GIT_DIR when reading the code which runs subprograms. This patch only makes the problem more obvious since readers now see the sequence:
(1) set GIT_DIR
(2) launch subprogram
So, a reader is more likely to wonder why GIT_DIR is needed for the subprogram and what possible wider side-effects it might have.
Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
Quoted 20 lines · Eric Sunshine
Quoted 9 lines · Eric Sunshine
Quoted 7 lines · Elijah Newren
Some comments on the various code changes:
* clone/push/fetch related:
* there are *many* subprocesses involved in fetch/push and friends, and they typically need to know theGIT_DIRthey are operating on
* this involves:fetch-patch.c,connected.c,bundle.c,clone.c,transport-helper.c,receive-pack.c,upload-pack.c
* this accounts for the majority of this patchQuoted 8 lines
It does feel a bit like a bandaid to insert new code at these locations to set
GIT_DIRmanually. It's not clear to readers whyGIT_DIRis needed for these specific cases, nor what the impact is when it is not set. Thus, one wonders if such a blanket approach is indeed required or if a more narrow and directed fix can be applied, such as calling subprograms with an explicit --git-dir= rather than settingGIT_DIRwith its potentially more broad and difficult-to-reason-about impact.
Quoted 6 lines
I meant to ask here what was the nature of the various failures you were seeing without
GIT_DIRbeing set, and whether you had considered tackling those failures with --git-dir= instead ofGIT_DIR. If so, were the problems too difficult to overcome by --git-dir= alone? Fleshing out the commit message with such information might be worthwhile.
clone, receive-pack, etc. will often spawn subprocesses such as repack, or index-pack. Those kinds of commands will not have been invoked from within a particular git directory, so discovery in the subprocess either doesn't find the git directory or finds the wrong one (e.g. testcases where you're in a repo and do git clone . my-clone would cause subprocesses to use discovery and find the outer git repo rather than the 'my-clone' they are supposed to be working on).
I didn't look at just --git-dir=. I probably could have, but since I was just trying to get a feel for how big of a change it was, and there's the possibility of nested subprocesses (clone forking some http-fetch thingy, which forks an index-pack subprocess, etc.), so I just did the environment variable to save the work from diving down the hierarchy.
I agree that if we want to make this kind of change, a better commit message would probably be in order, and perhaps trying out --git-dir= instead of setting the environment variable. However, after seeing the patch myself and thinking about it for another week, this just doesn't seem like a change that's worth it to me. If someone else wants to pick up the patch and run with it, they should feel free, but I'm not motivated enough to do so.
Quoted 10 lines
By the way, I also didn't mean to imply that the "difficult-to-reason-about
GIT_DIRinteraction in relation to subprograms" problem is new to this patch; it isn't. That problem has been around for ages (at least since 2007-08-01) but it wasn't as obvious since the setting ofGIT_DIRwas so far removed from other code which runs subprograms, thus readers likely wouldn't be thinking aboutGIT_DIRwhen reading the code which runs subprograms. This patch only makes the problem more obvious since readers now see the sequence:
Quoted 5 lines
(1) set
GIT_DIR
(2) launch subprogramSo, a reader is more likely to wonder why
GIT_DIRis needed for the subprogram and what possible wider side-effects it might have.
Yes, well put.
PATCH v5 8/11 stash: do not attempt to remove startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Since stash spawns a clean subprocess, make sure we run that from the startup_info->original_cwd directory, so that the clean processs knows to protect that directory. Also, since the clean command might no longer run from the toplevel, pass the ':/' magic pathspec to ensure we still clean from the toplevel.
--- builtin/stash.c | 4 +++- t/t2501-cwd-empty.sh | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-)
builtin/stash.c +3 -1
@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int qstruct child_process cp =CHILD_PROCESS_INIT ; cp.git_cmd =1 ; +if (startup_info->original_cwd) + cp.dir = startup_info->original_cwd;strvec_pushl (&cp.args,"clean" ,"--force" , -"--quiet" ,"-d" ,NULL ); +"--quiet" ,"-d" ,":/" ,NULL );if (include_untracked ==INCLUDE_ALL_FILES )strvec_push (&cp.args,"-x" );if (run_command (&cp)) {
t/t2501-cwd-empty.sh +1 -1
@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' ' ' test_expect_success 'stash does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal failure \ + test_incidental_untracked_dir_removal success \ git stash --include-untracked '
Signature · 1 line
gitgitgadget
PATCH v5 9/11 dir: avoid incidentally removing the original_cwd in remove_path()
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Modern git often tries to avoid leaving empty directories around when removing files. Originally, it did not bother. This behavior started with commit 80e21a9ed809 (merge-recursive::removeFile: remove empty directories, 2005-11-19), stating the reason simply as:
When the last file in a directory is removed as the result of a merge, try to rmdir the now-empty directory.
This was reimplemented in C and renamed to remove_path() in commit e1b3a2cad7 ("Build-in merge-recursive", 2008-02-07), but was still internal to merge-recursive.
This trend towards removing leading empty directories continued with commit d9b814cc97f1 (Add builtin "git rm" command, 2006-05-19), which stated the reasoning as:
The other question is what to do with leading directories. The old "git rm" script didn't do anything, which is somewhat inconsistent. This one will actually clean up directories that have become empty as a result of removing the last file, but maybe we want to have a flag to decide the behaviour?
remove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a function to remove as much as possible of a path, 2008-09-27), because it was noted that we had two separate implementations of the same idea AND both were buggy. It described the purpose of the function as
a function to remove as much as possible of a path
Why remove as much as possible? Well, at the time we probably would have said something like:
* removing leading directories makes things feel tidy
* removing leading directories doesn't hurt anything so long as they had no files in them.
But I don't believe those reasons hold when the empty directory happens to be the current working directory we inherited from our parent process. Leaving the parent process in a deleted directory can cause user confusion when subsequent processes fail: any git command, for example, will immediately fail with
fatal: Unable to read current working directory: No such file or directory
Other commands may similarly get confused. Modify remove_path() so that the empty leading directories it also deletes does not include the current working directory we inherited from our parent process. I have looked through every caller of remove_path() in the current codebase to make sure that all should take this change.
--- dir.c | 3 +++ dir.h | 6 +++++- t/t2501-cwd-empty.sh | 12 ++++-------- 3 files changed, 12 insertions(+), 9 deletions(-)
dir.c +3 -0
@@ -3327,6 +3327,9 @@ int remove_path(const char *name) slash = dirs + (slash - name);do { *slash ='\0' ; +if (startup_info->original_cwd && + !strcmp (startup_info->original_cwd, dirs)) +break ; }while (rmdir (dirs) ==0 && (slash =strrchr (dirs,'/' )));free (dirs); }
dir.h +5 -1
@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);*/ int remove_dir_recursively (struct strbuf *path,int flag); -/* tries to remove the path with empty directories along it, ignores ENOENT */ +/* +* Tries to remove the path, along with leading empty directories so long as +* those empty directories are not startup_info->original_cwd. Ignores +* ENOENT. +*/ int remove_path (const char *path);int fspathcmp (const char *a,const char *b);
t/t2501-cwd-empty.sh +4 -8
@@ -182,12 +182,12 @@ test_expect_success 'revert fails if cwd needs to be removed' ' ' test_expect_success 'rm does not clean cwd incidentally' ' - test_incidental_dir_removal failure git rm bar/baz.t + test_incidental_dir_removal success git rm bar/baz.t ' test_expect_success 'apply does not remove cwd incidentally' ' git diff HEAD HEAD~1 >patch && - test_incidental_dir_removal failure git apply ../patch + test_incidental_dir_removal success git apply ../patch ' test_incidental_untracked_dir_removal () {
@@ -271,12 +271,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet ) && test_path_is_missing a/b/c/tracked && - ## We would prefer if a/b was still present, though empty, since it - ## was the current working directory - #test_path_is_dir a/b - ## But the current behavior is that it not only deletes the directory - ## a/b as requested, but also goes and deletes a - test_path_is_missing a + test_path_is_missing a/b/c && + test_path_is_dir a/b ' test_expect_success 'git version continues working from a deleted dir' '
Signature · 1 line
gitgitgadget
PATCH v5 10/11 dir: new flag to remove_dir_recurse() to spare the original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
remove_dir_recurse(), and its non-static wrapper called remove_dir_recursively(), both take flags for modifying its behavior. As with the previous commits, we would generally like to protect the original_cwd, but we want to forced user commands (e.g. 'git rm -rf ...') or other special cases to remove it. Add a flag for this purpose. After reading through every caller of remove_dir_recursively() in the current codebase, there was only one that should be adjusted and that one only in a very unusual circumstance. Add a pair of new testcases to highlight that very specific case involving submodules && --git-dir && --work-tree.
--- builtin/rm.c | 3 ++- dir.c | 12 +++++++++--- dir.h | 3 +++ t/t2501-cwd-empty.sh | 5 ----- 4 files changed, 14 insertions(+), 9 deletions(-)
builtin/rm.c +2 -1
@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)if (!index_only) {int removed =0 , gitmodules_modified =0 ;struct strbuf buf =STRBUF_INIT ; +int flag = force ?REMOVE_DIR_PURGE_ORIGINAL_CWD :0 ;for (i =0 ; i < list.nr; i++) {const char *path = list.entry[i].name;if (list.entry[i].is_submodule) {strbuf_reset (&buf);strbuf_addstr (&buf, path); -if (remove_dir_recursively (&buf,0 )) +if (remove_dir_recursively (&buf, flag))die (_ ("could not remove '%s'" ), path); removed =1 ;
dir.c +9 -3
@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)int ret =0 , original_len = path->len, len, kept_down =0 ;int only_empty = (flag &REMOVE_DIR_EMPTY_ONLY );int keep_toplevel = (flag &REMOVE_DIR_KEEP_TOPLEVEL ); +int purge_original_cwd = (flag &REMOVE_DIR_PURGE_ORIGINAL_CWD );struct object_id submodule_head; if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)closedir (dir);strbuf_setlen (path, original_len); -if (!ret && !keep_toplevel && !kept_down) - ret = (!rmdir (path->buf) || errno ==ENOENT ) ?0 :-1 ; -else if (kept_up) +if (!ret && !keep_toplevel && !kept_down) { +if (!purge_original_cwd && + startup_info->original_cwd && + !strcmp (startup_info->original_cwd, path->buf)) + ret =-1 ;/* Do not remove current working directory */ +else + ret = (!rmdir (path->buf) || errno ==ENOENT ) ?0 :-1 ; + }else if (kept_up)/* * report the uplevel that it is not an error that we * did not rmdir() our directory.
dir.h +3 -0
@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);/* Remove the contents of path, but leave path itself. */ #define REMOVE_DIR_KEEP_TOPLEVEL 04 +/* Remove the_original_cwd too */ +#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08 +/* * Remove path and its contents, recursively. flags is a combination * of the above REMOVE_DIR_* constants. Return 0 on success.
t/t2501-cwd-empty.sh +0 -5
@@ -291,11 +291,6 @@ test_submodule_removal () { test_status= test "$path_status" = dir && test_status=test_must_fail - # Actually, while path_status=dir && test_status=test_must_fail - # reflect our desired behavior, current behavior is: - path_status=missing - test_status= - test_when_finished "git reset --hard HEAD~1" && test_when_finished "rm -rf .git/modules/my_submodule" &&
Signature · 1 line
gitgitgadget
PATCH v5 11/11 t2501: simplify the tests since we can now assume desired behavior
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
We no longer are dealing with a mixture of previous and desired behavior, so simplify the tests a bit.
--- t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------ 1 file changed, 36 insertions(+), 87 deletions(-)
t/t2501-cwd-empty.sh +36 -87
@@ -32,9 +32,6 @@ test_expect_success setup ' ' test_incidental_dir_removal () { - works=$1 && - shift && - test_when_finished "git reset --hard" && git checkout foo/bar/baz^{commit} &&
@@ -44,88 +41,57 @@ test_incidental_dir_removal () { cd foo && "$@" && - # Although we want pwd & git status to pass, test for existing - # rather than desired behavior. - if test "$works" = "success" - then - test-tool getcwd && - git status --porcelain - else - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Make sure foo still exists, and commands needing it work + test-tool getcwd && + git status --porcelain ) && test_path_is_missing foo/bar/baz && test_path_is_missing foo/bar && - # Although we want dir to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir foo - else - test_path_is_missing foo - fi + test_path_is_dir foo } test_required_dir_removal () { - works=$1 && - shift && - git checkout df_conflict^{commit} && test_when_finished "git clean -fdx" && ( cd dirORfile && - # We'd like for the command to fail (much as it would if there - # was an untracked file there), and for the index and worktree - # to be left clean with pwd and git status working afterwards. - # But test for existing rather than desired behavior. - if test "$works" = "success" - then - test_must_fail "$@" 2>../error && - grep "Refusing to remove.*current working directory" ../error && - - git diff --exit-code HEAD && - - test-tool getcwd && - git status --porcelain - else - "$@" && - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Ensure command refuses to run + test_must_fail "$@" 2>../error && + grep "Refusing to remove.*current working directory" ../error && + + # ...and that the index and working tree are left clean + git diff --exit-code HEAD && + + # Ensure that getcwd and git status do not error out (which + # they might if the current working directory had been removed) + test-tool getcwd && + git status --porcelain ) && - # Although we want dirORfile to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir dirORfile - else - test_path_is_file dirORfile - fi + test_path_is_dir dirORfile } test_expect_success 'checkout does not clean cwd incidentally' ' - test_incidental_dir_removal success git checkout init + test_incidental_dir_removal git checkout init ' test_expect_success 'checkout fails if cwd needs to be removed' ' - test_required_dir_removal success git checkout fd_conflict + test_required_dir_removal git checkout fd_conflict ' test_expect_success 'reset --hard does not clean cwd incidentally' ' - test_incidental_dir_removal success git reset --hard init + test_incidental_dir_removal git reset --hard init ' test_expect_success 'reset --hard fails if cwd needs to be removed' ' - test_required_dir_removal success git reset --hard fd_conflict + test_required_dir_removal git reset --hard fd_conflict ' test_expect_success 'merge does not clean cwd incidentally' ' - test_incidental_dir_removal success git merge reverted + test_incidental_dir_removal git merge reverted ' # This file uses some simple merges where
@@ -154,46 +120,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' GIT_TEST_MERGE_ALGORITHM=ort test_expect_success 'merge fails if cwd needs to be removed' ' - test_required_dir_removal success git merge fd_conflict + test_required_dir_removal git merge fd_conflict ' test_expect_success 'cherry-pick does not clean cwd incidentally' ' - test_incidental_dir_removal success git cherry-pick reverted + test_incidental_dir_removal git cherry-pick reverted ' test_expect_success 'cherry-pick fails if cwd needs to be removed' ' - test_required_dir_removal success git cherry-pick fd_conflict + test_required_dir_removal git cherry-pick fd_conflict ' test_expect_success 'rebase does not clean cwd incidentally' ' - test_incidental_dir_removal success git rebase reverted + test_incidental_dir_removal git rebase reverted ' test_expect_success 'rebase fails if cwd needs to be removed' ' - test_required_dir_removal success git rebase fd_conflict + test_required_dir_removal git rebase fd_conflict ' test_expect_success 'revert does not clean cwd incidentally' ' - test_incidental_dir_removal success git revert HEAD + test_incidental_dir_removal git revert HEAD ' test_expect_success 'revert fails if cwd needs to be removed' ' - test_required_dir_removal success git revert undo_fd_conflict + test_required_dir_removal git revert undo_fd_conflict ' test_expect_success 'rm does not clean cwd incidentally' ' - test_incidental_dir_removal success git rm bar/baz.t + test_incidental_dir_removal git rm bar/baz.t ' test_expect_success 'apply does not remove cwd incidentally' ' git diff HEAD HEAD~1 >patch && - test_incidental_dir_removal success git apply ../patch + test_incidental_dir_removal git apply ../patch ' test_incidental_untracked_dir_removal () { - works=$1 && - shift && - test_when_finished "git reset --hard" && git checkout foo/bar/baz^{commit} &&
@@ -205,38 +168,24 @@ test_incidental_untracked_dir_removal () { cd untracked && "$@" && - # Although we want pwd & git status to pass, test for existing - # rather than desired behavior. - if test "$works" = "success" - then - test-tool getcwd && - git status --porcelain - else - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Make sure untracked still exists, and commands needing it work + test-tool getcwd && + git status --porcelain ) && test_path_is_missing empty && test_path_is_missing untracked/random && - # Although we want dir to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir untracked - else - test_path_is_missing untracked - fi + test_path_is_dir untracked } test_expect_success 'clean does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal success \ + test_incidental_untracked_dir_removal \ git -C .. clean -fd -e warnings . >warnings && grep "Refusing to remove current working directory" warnings ' test_expect_success 'stash does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal success \ + test_incidental_untracked_dir_removal \ git stash --include-untracked '
Signature · 1 line
gitgitgadget
Quoted 11 lines · Elijah Newren via GitGitGadget
Traditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the
original_cwdas well.
I did not clearly voice my approval of the core idea here, but I do like it.
I think this fits squarely into a category of "help the user not get stuck" which Git has enough of those situations that we don't need this one. Even expert users won't know for sure if a 'git checkout' will cause their current directory to be removed, however unlikely.
In the Git project, we spend a lot of time in the root of our workdir, but this is not the typical case for large projects. I remember spending most of my time in a previous role working four levels deep in the directory hierarchy.
I read the previous two range-diffs and took another pass at this v5 and didn't see anything worth commenting on. This version is good to go.
There is _also_ more work to do, as follow-ups. In particular, the thing that I thought about was sparse-checkout and created this test which still fails at the end of your series (as an addition to t1092)
test_expect_success 'remove cwd' '
init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain && cd "$TRASH_DIRECTORY" && test_cmp expect actual ) done
'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Thanks,
-Stolee
Quoted 49 lines · Derrick Stolee
Quoted 11 lines · Elijah Newren via GitGitGadget
Traditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the
original_cwdas well.I did not clearly voice my approval of the core idea here, but I do like it.
I think this fits squarely into a category of "help the user not get stuck" which Git has enough of those situations that we don't need this one. Even expert users won't know for sure if a 'git checkout' will cause their current directory to be removed, however unlikely.
In the Git project, we spend a lot of time in the root of our workdir, but this is not the typical case for large projects. I remember spending most of my time in a previous role working four levels deep in the directory hierarchy.
I read the previous two range-diffs and took another pass at this v5 and didn't see anything worth commenting on. This version is good to go.
There is _also_ more work to do, as follow-ups. In particular, the thing that I thought about was sparse-checkout and created this test which still fails at the end of your series (as an addition to t1092)
test_expect_success'remove cwd' 'init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain && cd "$TRASH_DIRECTORY" && test_cmp expect actual ) done
Quoted 4 lines
'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Not to pile on about "the core idea", just a question while this is fresh in your mind:
I think that those cases would per [1] be ones where a more isolated change of reading the $PWD from the environment would make all those commands work as expected. Or would the "$TRASH_DIRECTORY" also otherwise go away in this examples?
Anyway, just per [1] and the potential future follow-ups is this (I don't think so, but maybe I'm wrong) or other examples you have things that specifically need the "retain the getcwd()" part of this series?
Or just (as I think would be the case with that "git status") to not have setup.c die quite as eagerly as it does now when getcwd() fails, but it can find its way back to the .git via the environment's $PWD?
Also note that writing any tests to check the current behavior is a pain due to the bin-wrappers as I discussed in some previous threads, but that's a test-only oddity, and won't impact how git would behave once installed (although now it doesn't behave well at all). So probing the current behavior via tests is hard, unless you use GIT_TEST_INSTALLED to get around the bin-wrappers.
There *are* definitely cases where not-just-that-setup.c change but also the "don't remove the CWD" is an inherently better & more complete solution. But I think that's mainly to do with 3rd party shellscripts & other programs outside of our control.
I'm assuming that you were working with this on Windows, where presumably there's fewer/none such shellscripts you rely on, but that's now two presumes in a row, so... :)
Quoted 24 lines · Ævar Arnfjörð Bjarmason
Quoted 17 lines · Derrick Stolee
test_expect_success'remove cwd' 'init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain && cd "$TRASH_DIRECTORY" && test_cmp expect actual ) doneQuoted 4 lines
'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Quoted 7 lines
Not to pile on about "the core idea", just a question while this is fresh in your mind:
I think that those cases would per [1] be ones where a more isolated change of reading the $PWD from the environment would make all those commands work as expected. Or would the "$TRASH_DIRECTORY" also otherwise go away in this examples?
I have read this message and the one you are referring two twice and I cannot understand what you are trying to say here.
Quoted 7 lines
Anyway, just per [1] and the potential future follow-ups is this (I don't think so, but maybe I'm wrong) or other examples you have things that specifically need the "retain the
getcwd()" part of this series?Or just (as I think would be the case with that "git status") to not have
setup.cdie quite as eagerly as it does now whengetcwd()fails, but it can find its way back to the .git via the environment's $PWD?
Are you implying that Git will be the only thing broken by a missing directory after we leave in this state? I doubt that is true, and we should be good citizens here by leaving the directory around.
Quoted 4 lines
There *are* definitely cases where
not-just-that-setup.cchange but also the "don't remove the CWD" is an inherently better & more complete solution. But I think that's mainly to do with 3rd party shellscripts & other programs outside of our control.
Exactly. We should take this change because it is valuable to not cause a confusing error in other tools.
Quoted 3 lines
I'm assuming that you were working with this on Windows, where presumably there's fewer/none such shellscripts you rely on, but that's now two presumes in a row, so... :)
I'm working on Ubuntu, where I do all of my Git development unless there is a platform specific reason to do so.
Thanks,
-Stolee
Quoted 28 lines · Derrick Stolee
Quoted 27 lines · Ævar Arnfjörð Bjarmason
Quoted 17 lines · Derrick Stolee
test_expect_success'remove cwd' 'init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain && cd "$TRASH_DIRECTORY" && test_cmp expect actual ) doneQuoted 4 lines
'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Not to pile on about "the core idea", just a question while this is fresh in your mind:
Quoted 7 lines
Quoted 4 lines
I think that those cases would per [1] be ones where a more isolated change of reading the $PWD from the environment would make all those commands work as expected. Or would the "$TRASH_DIRECTORY" also otherwise go away in this examples?
I have read this message and the one you are referring two twice and I cannot understand what you are trying to say here.
I'm asking whether the WIP patch I posted at
https://lore.kernel.org/git/[email protected]/
would partially/entirely solve those issues you mentiened (but I see Elijah has a side-reply saying it might not in either approach).
Quoted 11 lines
Quoted 7 lines
Anyway, just per [1] and the potential future follow-ups is this (I don't think so, but maybe I'm wrong) or other examples you have things that specifically need the "retain the
getcwd()" part of this series?Or just (as I think would be the case with that "git status") to not have
setup.cdie quite as eagerly as it does now whengetcwd()fails, but it can find its way back to the .git via the environment's $PWD?Are you implying that Git will be the only thing broken by a missing directory after we leave in this state? I doubt that is true, and we should be good citizens here by leaving the directory around.
No, I'm just trying to clarify which specific thing we'd need in this/other cases to fix things *in git*. Because...
Quoted 7 lines
Quoted 4 lines
There *are* definitely cases where
not-just-that-setup.cchange but also the "don't remove the CWD" is an inherently better & more complete solution. But I think that's mainly to do with 3rd party shellscripts & other programs outside of our control.Exactly. We should take this change because it is valuable to not cause a confusing error in other tools.
...maybe we should fix things for other tools too, but for any future development it helps to know what was needed to fix what issues.
Quoted 6 lines
Quoted 3 lines
I'm assuming that you were working with this on Windows, where presumably there's fewer/none such shellscripts you rely on, but that's now two presumes in a row, so... :)
I'm working on Ubuntu, where I do all of my Git development unless there is a platform specific reason to do so.
*nod*, badly assumed then :)
Quoted 26 lines · Derrick Stolee
Quoted 11 lines · Elijah Newren via GitGitGadget
Traditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the
original_cwdas well.I did not clearly voice my approval of the core idea here, but I do like it.
I think this fits squarely into a category of "help the user not get stuck" which Git has enough of those situations that we don't need this one. Even expert users won't know for sure if a 'git checkout' will cause their current directory to be removed, however unlikely.
In the Git project, we spend a lot of time in the root of our workdir, but this is not the typical case for large projects. I remember spending most of my time in a previous role working four levels deep in the directory hierarchy.
Quoted 6 lines
I read the previous two range-diffs and took another pass at this v5 and didn't see anything worth commenting on. This version is good to go.
There is _also_ more work to do, as follow-ups. In particular, the thing that I thought about was sparse-checkout and created this test which still fails at the end of your series (as an addition to t1092)
Interesting testcase...
Quoted 13 lines
test_expect_success'remove cwd' 'init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain &&
However, this line is broken even if the directory weren't removed. Not because of the "git status --porcelain" part, but because of two other reasons:
1) test_sparse_match presumes it is run from the directory above the repos while you still have it in $repo/deep/deeper1
2) The point of test_sparse_match is to compare the results in two different repositories, but it'll do this twice and the first time without the changes having been made to the sparse-index repo. Perhaps this belonged outside the surrounding for loop?
I think I'd either drop the "test_sparse_match" or else just drop the whole line; the real comparison is the expect/actual files. Dropping this line makes it a good test.
Quoted 8 lines
cd "$TRASH_DIRECTORY" && test_cmp expect actual ) done'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Yeah, sounds good. Since you piqued my interest, though, the problem is that we're passing an absolute path to remove_dir_recursively() inside clean_tracked_sparse_directories() when we should be passing a relative path. (We always chdir(r->worktree) in setup.c, so there's no need to prepend the path with r->worktree+'/'.)
Still, the current series is long enough and unless there are issues others have spotted with it, I'd rather just let it proceed as-is and then send this fix and a correction of your testcase in separately.
Quoted 25 lines · Elijah Newren
Quoted 3 lines · Derrick Stolee
There is _also_ more work to do, as follow-ups. In particular, the thing that I thought about was sparse-checkout and created this test which still fails at the end of your series (as an addition to t1092)
Interesting testcase...
Quoted 13 lines
test_expect_success'remove cwd' 'init_repos && test_sparse_match git sparse-checkout set deep/deeper1/deepest && for repo in sparse-checkout sparse-index do ( cd $repo/deep/deeper1 && test-tool getcwd >"$TRASH_DIRECTORY/expect" && git sparse-checkout set && test-tool getcwd >"$TRASH_DIRECTORY/actual" && test_sparse_match git status --porcelain &&However, this line is broken even if the directory weren't removed. Not because of the "git status --porcelain" part, but because of two other reasons:
Quoted 6 lines
1)
test_sparse_matchpresumes it is run from the directory above the repos while you still have it in $repo/deep/deeper1
2) The point oftest_sparse_matchis to compare the results in two different repositories, but it'll do this twice and the first time without the changes having been made to the sparse-index repo. Perhaps this belonged outside the surrounding for loop?
You're right! I had first written this test with test_sparse_match everywhere, but test_sparse_match uses subshells to 'cd' into different places, so it doesn't let us get a failure in the 'test-tool getcwd'.
I missed this one because the test fails with 'test-tool getcwd' so I never get to the broken line.
Quoted 3 lines
I think I'd either drop the "
test_sparse_match" or else just drop the whole line; the real comparison is the expect/actual files. Dropping this line makes it a good test.
I agree.
Quoted 15 lines
Quoted 8 lines
cd "$TRASH_DIRECTORY" && test_cmp expect actual ) done'
Please do not let this test delay the advancement of this series. As we find these kinds of issues, we can fix them one-by-one as needed.
Yeah, sounds good. Since you piqued my interest, though, the problem is that we're passing an absolute path to
remove_dir_recursively()insideclean_tracked_sparse_directories()when we should be passing a relative path. (We alwayschdir(r->worktree)insetup.c, so there's no need to prepend the path with r->worktree+'/'.)
Quoted 3 lines
Still, the current series is long enough and unless there are issues others have spotted with it, I'd rather just let it proceed as-is and then send this fix and a correction of your testcase in separately.
Absolutely. Let's pick up this fix another time.
Thanks,
-Stolee
v6 cover Avoid removing the current working directory, even if it becomes empty
In reply to Elijah Newren via GitGitGadgetTraditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the original_cwd as well.
Peff and Junio provided some good pros/cons, if it helps:
* Pros: Peff (original suggester of the idea)[2], and Junio[3]
* Cons: Peff [2, again -- see the "P.S."], and Junio[4]
[1] well, with a few exceptions; see
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/ [4]
https://lore.kernel.org/git/[email protected]/
Changes since v5:
* It's been about a week without changes, so including Acks that have come in (let me know if I was wrong in my interpretation of these statements as Acks):
* from Stolee ("This version is good to go",
https://lore.kernel.org/git/[email protected]/) * from Ævar ("I've got no objections to these changes going in as they stand.", https://lore.kernel.org/git/[email protected]/)
Changes since v4:
* actually fix bashism
Changes since v3:
* fixed one codepath from v2 so that the series really is only about the working tree
* used test-tool getcwd instead of pwd -P as suggested by Ævar for some less common platforms
* fixed bashism
* check for clean index/worktree after verifying that expected-to-abort codepaths do abort, to make it clearer that we expect an early abort
* remove a leftover (and confusing) is_absolute_dir() check in sequencer and stash from an earlier round of the series
Changes since v2:
* the series is now only about the working tree. So if the original cwd is outside the worktree (or we're in a bare repo), then the new code is a no-op.
* fixed ugly early die() possibility (uses strbuf_getcwd() instead of xgetcwd())
* modified the initial tests to show both expected and desired behavior. subsequent patches fix the tests. One new patch added at the end which simplifies the tests to only check for desired behavior.
* NULLify startup_info->original_cwd when it matches the toplevel worktree; that is already protected and we don't need secondary protection for it. This simplified some other codepaths so we don't have to check for startup_info->original_cwd == "".
* clarified some commit messages
Changes since v1:
* clarified multiple commit messages
* renamed the_cwd to startup_info->original_cwd to make it clearer that it's our parent process'es cwd that really matters, which we inherited at program startup. Also pulls it out of the global namespace.
* Normalize the path for startup_info->original_cwd, and ensure that it's actually the original cwd even if -C is passed to git.
* small code cleanups suggested by René and Ævar
* split the final patch (which got the most comments) into two -- one for each function being modified. Also, add a bunch more history to the first of the two resulting commit messages
* no longer has a content conflict with so/stash-staged
* add another value for the flags parameter that remove_dir_recursively() takes so that it can opt into either the old or the new behavior. Use that for the one special corner case I could find where it matters, and add a few tests around it to highlight the utility of the flag.
Elijah Newren (11):
t2501: add various tests for removing the current working directory
setup: introduce startup_info->original_cwd
unpack-trees: refuse to remove startup_info->original_cwd
unpack-trees: add special cwd handling
symlinks: do not include startup_info->original_cwd in dir removal
clean: do not attempt to remove startup_info->original_cwd
rebase: do not attempt to remove startup_info->original_cwd
stash: do not attempt to remove startup_info->original_cwd
dir: avoid incidentally removing the original_cwd in remove_path()
dir: new flag to remove_dir_recurse() to spare the original_cwd
t2501: simplify the tests since we can now assume desired behavior
builtin/clean.c | 44 +++++-- builtin/rm.c | 3 +- builtin/stash.c | 4 +- cache.h | 2 + common-main.c | 4 + dir.c | 15 ++- dir.h | 9 +- sequencer.c | 2 + setup.c | 65 ++++++++++ symlinks.c | 8 +- t/t2501-cwd-empty.sh | 277 +++++++++++++++++++++++++++++++++++++++++++ unpack-trees.c | 30 ++++- unpack-trees.h | 1 + 13 files changed, 442 insertions(+), 22 deletions(-) create mode 100755 t/t2501-cwd-empty.sh
base-commit: 88d915a634b449147855041d44875322de2b286d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v6
Pull-Request: https://github.com/git/git/pull/1140
Range-diff vs v5:
1: 7c72e888d2e ! 1: 1fd76d4f8a8 t2501: add various tests for removing the current working directory
@@ Commit message
Also add a few tests suggested during the review of earlier rounds of
this patch series.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## t/t2501-cwd-empty.sh (new) ##
2: 37f333b2024 ! 2: cd1f564c4c0 setup: introduce startup_info->original_cwd
@@ Commit message
Subsequent commits will make use of this new variable.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## cache.h ##
3: b611c73bd15 ! 3: 4a1f62b662c unpack-trees: refuse to remove startup_info->original_cwd
@@ Commit message
error out if the directory is the current working directory we inherited from our parent process.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## t/t2501-cwd-empty.sh ##
4: 706415a4547 ! 4: 03f2b0b8fb7 unpack-trees: add special cwd handling
@@ Commit message
there is no untracked checking to be done, so we simply add a special case near the top of verify_absent_1.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## t/t2501-cwd-empty.sh ##
5: 66ef6b4d943 ! 5: e75f6c3f9ad symlinks: do not include startup_info->original_cwd in dir removal
@@ Commit message
commands) that would otherwise report confusing messages about being
unable to read the current working directory.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## symlinks.c ##
6: 54d059c683e ! 6: d4f8784a6e4 clean: do not attempt to remove startup_info->original_cwd
@@ Metadata
## Commit message ##
clean: do not attempt to remove startup_info->original_cwd
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## builtin/clean.c ##
7: f8efb7446c3 ! 7: fd500cc1843 rebase: do not attempt to remove startup_info->original_cwd
@@ Commit message
the startup_info->original_cwd directory, so that the checkout process knows to protect that directory.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## sequencer.c ##
8: 2855ed45112 ! 8: 6f3c4fd49b9 stash: do not attempt to remove startup_info->original_cwd
@@ Commit message
longer run from the toplevel, pass the ':/' magic pathspec to ensure we still clean from the toplevel.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## builtin/stash.c ##
9: 23dfc3e399d ! 9: 1a66b1a2386 dir: avoid incidentally removing the original_cwd in remove_path()
@@ Commit message
looked through every caller of remove_path() in the current codebase to
make sure that all should take this change.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## dir.c ##
10: fe47c0f0c17 ! 10: 4a18efd51ef dir: new flag to remove_dir_recurse() to spare the original_cwd
@@ Commit message
highlight that very specific case involving submodules && --git-dir && --work-tree.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## builtin/rm.c ##
11: 431dd651a7e ! 11: 63781ed8b8e t2501: simplify the tests since we can now assume desired behavior
@@ Commit message
We no longer are dealing with a mixture of previous and desired
behavior, so simplify the tests a bit.
+ Acked-by: Derrick Stolee <[email protected]>
+ Acked-by: Ævar Arnfjörð Bjarmason <[email protected]>
## t/t2501-cwd-empty.sh ##
Signature · 1 line
gitgitgadget
PATCH v6 1/11 t2501: add various tests for removing the current working directory
From: Elijah Newren <[email protected]>
Numerous commands will remove directories left empty as a "convenience" after removing files within them. That is normally fine, but removing the current working directory can be rather inconvenient since it can cause confusion for the user when they run subsequent commands. For example, after one git process has removed the current working directory, git status/log/diff will all abort with the message:
fatal: Unable to read current working directory: No such file or directory
We also have code paths that, when a file needs to be placed where a directory is (due to e.g. checkout, merge, reset, whatever), will check if this is okay and error out if not. These rules include:
* all tracked files under that directory are intended to be removed by the operation
* none of the tracked files under that directory have uncommitted modification
* there are no untracked files under that directory
However, if we end up remove the current working directory, we can cause user confusion when they run subsequent commands, so we would prefer if there was a fourth rule added to this list: avoid removing the current working directory.
Since there are several code paths that can result in the current working directory being removed, add several tests of various different codepaths. To make it clearer what the difference between the current behavior and the behavior at the end of the series, code both of them into the tests and have the appropriate behavior be selected by a flag. Subsequent commits will toggle the flag from current to desired behavior.
Also add a few tests suggested during the review of earlier rounds of this patch series.
--- t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100755 t/t2501-cwd-empty.sh
t/t2501-cwd-empty.sh new file +342 -0
@@ -0,0 +1,342 @@ +#!/bin/sh + +test_description='Test handling of the current working directory becoming empty' + +. ./test-lib.sh + +test_expect_success setup ' + test_commit init && + + git branch fd_conflict && + + mkdir -p foo/bar && + test_commit foo/bar/baz && + + git revert HEAD && + git tag reverted && + + git checkout fd_conflict && + mkdir dirORfile && + test_commit dirORfile/foo && + + git rm -r dirORfile && + echo not-a-directory >dirORfile && + git add dirORfile && + git commit -m dirORfile && + + git switch -c df_conflict HEAD~1 && + test_commit random_file && + + git switch -c undo_fd_conflict fd_conflict && + git revert HEAD +' + +test_incidental_dir_removal () { + works=$1 && + shift && + + test_when_finished "git reset --hard" && + + git checkout foo/bar/baz^{commit} && + test_path_is_dir foo/bar && + + ( + cd foo && + "$@" && + + # Although we want pwd & git status to pass, test for existing + # rather than desired behavior. + if test "$works" = "success" + then + test-tool getcwd && + git status --porcelain + else + ! test-tool getcwd && + test_might_fail git status --porcelain + fi + ) && + test_path_is_missing foo/bar/baz && + test_path_is_missing foo/bar && + + # Although we want dir to be present, test for existing rather + # than desired behavior. + if test "$works" = "success" + then + test_path_is_dir foo + else + test_path_is_missing foo + fi +} + +test_required_dir_removal () { + works=$1 && + shift && + + git checkout df_conflict^{commit} && + test_when_finished "git clean -fdx" && + + ( + cd dirORfile && + + # We'd like for the command to fail (much as it would if there + # was an untracked file there), and for the index and worktree + # to be left clean with pwd and git status working afterwards. + # But test for existing rather than desired behavior. + if test "$works" = "success" + then + test_must_fail "$@" 2>../error && + grep "Refusing to remove.*current working directory" ../error && + + git diff --exit-code HEAD && + + test-tool getcwd && + git status --porcelain + else + "$@" && + ! test-tool getcwd && + test_might_fail git status --porcelain + fi + ) && + + # Although we want dirORfile to be present, test for existing rather + # than desired behavior. + if test "$works" = "success" + then + test_path_is_dir dirORfile + else + test_path_is_file dirORfile + fi +} + +test_expect_success 'checkout does not clean cwd incidentally' ' + test_incidental_dir_removal failure git checkout init +' + +test_expect_success 'checkout fails if cwd needs to be removed' ' + test_required_dir_removal failure git checkout fd_conflict +' + +test_expect_success 'reset --hard does not clean cwd incidentally' ' + test_incidental_dir_removal failure git reset --hard init +' + +test_expect_success 'reset --hard fails if cwd needs to be removed' ' + test_required_dir_removal failure git reset --hard fd_conflict +' + +test_expect_success 'merge does not clean cwd incidentally' ' + test_incidental_dir_removal failure git merge reverted +' + +# This file uses some simple merges where +# Base: 'dirORfile/' exists +# Side1: random other file changed +# Side2: 'dirORfile/' removed, 'dirORfile' added +# this should resolve cleanly, but merge-recursive throws merge conflicts +# because it's dumb. Add a special test for checking merge-recursive (and +# merge-ort), then after this just hard require ort for all remaining tests. +# +test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' ' + git checkout foo/bar/baz && + test_when_finished "git clean -fdx" && + + mkdir dirORfile && + ( + cd dirORfile && + + # We would rather this failed, but we test for existing + # rather than desired behavior + git merge fd_conflict 2>../error + ) && + + ## Here is the behavior we would rather have: + #test_path_is_dir dirORfile && + #grep "Refusing to remove the current working directory" error + ## But instead we test for existing behavior + test_path_is_file dirORfile && + test_must_be_empty error +' + +GIT_TEST_MERGE_ALGORITHM=ort + +test_expect_success 'merge fails if cwd needs to be removed' ' + test_required_dir_removal failure git merge fd_conflict +' + +test_expect_success 'cherry-pick does not clean cwd incidentally' ' + test_incidental_dir_removal failure git cherry-pick reverted +' + +test_expect_success 'cherry-pick fails if cwd needs to be removed' ' + test_required_dir_removal failure git cherry-pick fd_conflict +' + +test_expect_success 'rebase does not clean cwd incidentally' ' + test_incidental_dir_removal failure git rebase reverted +' + +test_expect_success 'rebase fails if cwd needs to be removed' ' + test_required_dir_removal failure git rebase fd_conflict +' + +test_expect_success 'revert does not clean cwd incidentally' ' + test_incidental_dir_removal failure git revert HEAD +' + +test_expect_success 'revert fails if cwd needs to be removed' ' + test_required_dir_removal failure git revert undo_fd_conflict +' + +test_expect_success 'rm does not clean cwd incidentally' ' + test_incidental_dir_removal failure git rm bar/baz.t +' + +test_expect_success 'apply does not remove cwd incidentally' ' + git diff HEAD HEAD~1 >patch && + test_incidental_dir_removal failure git apply ../patch +' + +test_incidental_untracked_dir_removal () { + works=$1 && + shift && + + test_when_finished "git reset --hard" && + + git checkout foo/bar/baz^{commit} && + mkdir -p untracked && + mkdir empty + >untracked/random && + + ( + cd untracked && + "$@" && + + # Although we want pwd & git status to pass, test for existing + # rather than desired behavior. + if test "$works" = "success" + then + test-tool getcwd && + git status --porcelain + else + ! test-tool getcwd && + test_might_fail git status --porcelain + fi + ) && + test_path_is_missing empty && + test_path_is_missing untracked/random && + + # Although we want dir to be present, test for existing rather + # than desired behavior. + if test "$works" = "success" + then + test_path_is_dir untracked + else + test_path_is_missing untracked + fi +} + +test_expect_success 'clean does not remove cwd incidentally' ' + test_incidental_untracked_dir_removal failure \ + git -C .. clean -fd -e warnings . >warnings +' + +test_expect_success 'stash does not remove cwd incidentally' ' + test_incidental_untracked_dir_removal failure \ + git stash --include-untracked +' + +test_expect_success '`rm -rf dir` only removes a subset of dir' ' + test_when_finished "rm -rf a/" && + + mkdir -p a/b/c && + >a/b/c/untracked && + >a/b/c/tracked && + git add a/b/c/tracked && + + ( + cd a/b && + git rm -rf ../b + ) && + + test_path_is_dir a/b && + test_path_is_missing a/b/c/tracked && + test_path_is_file a/b/c/untracked +' + +test_expect_success '`rm -rf dir` even with only tracked files will remove something else' ' + test_when_finished "rm -rf a/" && + + mkdir -p a/b/c && + >a/b/c/tracked && + git add a/b/c/tracked && + + ( + cd a/b && + git rm -rf ../b + ) && + + test_path_is_missing a/b/c/tracked && + ## We would prefer if a/b was still present, though empty, since it + ## was the current working directory + #test_path_is_dir a/b + ## But the current behavior is that it not only deletes the directory + ## a/b as requested, but also goes and deletes a + test_path_is_missing a +' + +test_expect_success 'git version continues working from a deleted dir' ' + mkdir tmp && + ( + cd tmp && + rm -rf ../tmp && + git version + ) +' + +test_submodule_removal () { + path_status=$1 && + shift && + + test_status= + test "$path_status" = dir && test_status=test_must_fail + + # Actually, while path_status=dir && test_status=test_must_fail + # reflect our desired behavior, current behavior is: + path_status=missing + test_status= + + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf .git/modules/my_submodule" && + + git checkout foo/bar/baz && + + git init my_submodule && + touch my_submodule/file && + git -C my_submodule add file && + git -C my_submodule commit -m "initial commit" && + git submodule add ./my_submodule && + git commit -m "Add the submodule" && + + ( + cd my_submodule && + $test_status "$@" + ) && + + test_path_is_${path_status} my_submodule +} + +test_expect_success 'rm -r with -C leaves submodule if cwd inside' ' + test_submodule_removal dir git -C .. rm -r my_submodule/ +' + +test_expect_success 'rm -r leaves submodule if cwd inside' ' + test_submodule_removal dir \ + git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/ +' + +test_expect_success 'rm -rf removes submodule even if cwd inside' ' + test_submodule_removal missing \ + git --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/ +' + +test_done
Signature · 1 line
gitgitgadget
Re: [PATCH v6 01/11] t2501: add various tests for removing the current working directory
t/t2501-cwd-empty.sh line 294
Quoted 10 lines · Elijah Newren via GitGitGadget
From: Elijah Newren <[email protected]>
[...]t/t2501-cwd-empty.sh new file +8
-0+test_expect_success 'git version continues working from a deleted dir' ' + mkdir tmp && + ( + cd tmp && + rm -rf ../tmp && + git version + ) +'
There's a (minor) regression here in v2.35.0 on AIX (as of 8a0d52dfd87 (t2501: add various tests for removing the current working directory, 2021-12-09)). The tooling on that OS will also refuse to remove the cwd:
+ mkdir tmp
+ cd tmp
+ rm -rf ../tmp
rm: Cannot remove the current directory ../tmp.
error: last command exited with $?=2
not ok 21 - git version continues working from a deleted dir #
Just splitting that into a prereq that checks if the first two lines of the sub-shell works should be a solution in this case, but I only had time for this quick bug report :)
PATCH v6 2/11 setup: introduce startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Removing the current working directory causes all subsequent git commands run from that directory to get confused and fail with a message about being unable to read the current working directory:
$ git status
fatal: Unable to read current working directory: No such file or directoryNon-git commands likely have similar warnings or even errors, e.g.
$ bash -c 'echo hello'
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
helloThis confuses end users, particularly since the command they get the error from is not the one that caused the problem; the problem came from the side-effect of some previous command.
We would like to avoid removing the current working directory of our parent process; towards this end, introduce a new variable, startup_info->original_cwd, that tracks the current working directory that we inherited from our parent process. For convenience of later comparisons, we prefer that this new variable store a path relative to the toplevel working directory (thus much like 'prefix'), except without the trailing slash.
Subsequent commits will make use of this new variable.
--- cache.h | 2 ++ common-main.c | 4 ++++ setup.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+)
cache.h +2 -0
@@ -1834,8 +1834,10 @@ void overlay_tree_on_index(struct index_state *istate,struct startup_info {int have_repository;const char *prefix; +const char *original_cwd; };extern struct startup_info *startup_info; +extern const char *tmp_original_cwd;/* merge.c */ struct commit_list ;
common-main.c +4 -0
@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)int main (int argc,const char **argv) {int result; +struct strbuf tmp =STRBUF_INIT ;trace2_initialize_clock ();
@@ -49,6 +50,9 @@ int main(int argc, const char **argv)trace2_cmd_start (argv);trace2_collect_process_info (TRACE2_PROCESS_INFO_STARTUP ); +if (!strbuf_getcwd (&tmp)) + tmp_original_cwd =strbuf_detach (&tmp,NULL ); + result =cmd_main (argc, argv);trace2_cmd_exit (result);
setup.c +65 -0
83 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v6 3/11 unpack-trees: refuse to remove startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
In the past, when a directory needs to be removed to make room for a file, we have always errored out when that directory contains any untracked (but not ignored) files. Add an extra condition on that: also error out if the directory is the current working directory we inherited from our parent process.
--- t/t2501-cwd-empty.sh | 20 +++++++------------- unpack-trees.c | 17 +++++++++++++---- unpack-trees.h | 1 + 3 files changed, 21 insertions(+), 17 deletions(-)
t/t2501-cwd-empty.sh +7 -13
@@ -113,7 +113,7 @@ test_expect_success 'checkout does not clean cwd incidentally' ' ' test_expect_success 'checkout fails if cwd needs to be removed' ' - test_required_dir_removal failure git checkout fd_conflict + test_required_dir_removal success git checkout fd_conflict ' test_expect_success 'reset --hard does not clean cwd incidentally' '
@@ -144,23 +144,17 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' ( cd dirORfile && - # We would rather this failed, but we test for existing - # rather than desired behavior - git merge fd_conflict 2>../error + test_must_fail git merge fd_conflict 2>../error ) && - ## Here is the behavior we would rather have: - #test_path_is_dir dirORfile && - #grep "Refusing to remove the current working directory" error - ## But instead we test for existing behavior - test_path_is_file dirORfile && - test_must_be_empty error + test_path_is_dir dirORfile && + grep "Refusing to remove the current working directory" error ' GIT_TEST_MERGE_ALGORITHM=ort test_expect_success 'merge fails if cwd needs to be removed' ' - test_required_dir_removal failure git merge fd_conflict + test_required_dir_removal success git merge fd_conflict ' test_expect_success 'cherry-pick does not clean cwd incidentally' '
@@ -168,7 +162,7 @@ test_expect_success 'cherry-pick does not clean cwd incidentally' ' ' test_expect_success 'cherry-pick fails if cwd needs to be removed' ' - test_required_dir_removal failure git cherry-pick fd_conflict + test_required_dir_removal success git cherry-pick fd_conflict ' test_expect_success 'rebase does not clean cwd incidentally' '
@@ -184,7 +178,7 @@ test_expect_success 'revert does not clean cwd incidentally' ' ' test_expect_success 'revert fails if cwd needs to be removed' ' - test_required_dir_removal failure git revert undo_fd_conflict + test_required_dir_removal success git revert undo_fd_conflict ' test_expect_success 'rm does not clean cwd incidentally' '
unpack-trees.c +13 -4
@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {/* ERROR_NOT_UPTODATE_DIR */ "Updating '%s' would lose untracked files in it" , +/* ERROR_CWD_IN_THE_WAY */ +"Refusing to remove '%s' since it is the current working directory." , +/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */ "Untracked working tree file '%s' would be overwritten by merge.",
@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, msgs[ERROR_NOT_UPTODATE_DIR ] =_ ("Updating the following directories would lose untracked files in them:\n%s" ); + msgs[ERROR_CWD_IN_THE_WAY ] = +_ ("Refusing to remove the current working directory:\n%s" ); +if (!strcmp (cmd,"checkout" )) msg =advice_enabled (ADVICE_COMMIT_BEFORE_MERGE ) ? _("The following untracked working tree files would be removed by checkout:\n%%s"
@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce, cnt++; } -/* -* Then we need to make sure that we do not lose a locally -* present file that is not ignored. -*/ +/* Do not lose a locally present file that is not ignored. */ pathbuf =xstrfmt ("%.*s/" , namelen, ce->name);memset (&d,0 ,sizeof (d));
@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,free (pathbuf);if (i)return add_rejected_path (o,ERROR_NOT_UPTODATE_DIR , ce->name); + +/* Do not lose startup_info->original_cwd */ +if (startup_info->original_cwd && + !strcmp (startup_info->original_cwd, ce->name)) +return add_rejected_path (o,ERROR_CWD_IN_THE_WAY , ce->name); +return cnt; }
unpack-trees.h +1 -0
@@ -19,6 +19,7 @@ enum unpack_trees_error_types {ERROR_WOULD_OVERWRITE =0 ,ERROR_NOT_UPTODATE_FILE ,ERROR_NOT_UPTODATE_DIR , +ERROR_CWD_IN_THE_WAY ,ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN ,ERROR_WOULD_LOSE_UNTRACKED_REMOVED , ERROR_BIND_OVERLAP,
Signature · 1 line
gitgitgadget
PATCH v6 4/11 unpack-trees: add special cwd handling
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
When running commands such as git reset --hard from a subdirectory, if that subdirectory is in the way of adding needed files, bail with an error message.
Note that this change looks kind of like it duplicates the new lines of code from the previous commit in verify_clean_subdirectory(). However, when we are preserving untracked files, we would rather any error messages about untracked files being in the way take precedence over error messages about a subdirectory that happens to be the_original_cwd being in the way. But in the UNPACK_RESET_OVERWRITE_UNTRACKED case, there is no untracked checking to be done, so we simply add a special case near the top of verify_absent_1.
--- t/t2501-cwd-empty.sh | 2 +- unpack-trees.c | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-)
t/t2501-cwd-empty.sh +1 -1
@@ -121,7 +121,7 @@ test_expect_success 'reset --hard does not clean cwd incidentally' ' ' test_expect_success 'reset --hard fails if cwd needs to be removed' ' - test_required_dir_removal failure git reset --hard fd_conflict + test_required_dir_removal success git reset --hard fd_conflict ' test_expect_success 'merge does not clean cwd incidentally' '
unpack-trees.c +11 -2
@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,int len;struct stat st; -if (o->index_only || !o->update || - o->reset ==UNPACK_RESET_OVERWRITE_UNTRACKED ) +if (o->index_only || !o->update)return 0 ; +if (o->reset ==UNPACK_RESET_OVERWRITE_UNTRACKED ) { +/* Avoid nuking startup_info->original_cwd... */ +if (startup_info->original_cwd && + !strcmp (startup_info->original_cwd, ce->name)) +return add_rejected_path (o,ERROR_CWD_IN_THE_WAY , + ce->name); +/* ...but nuke anything else. */ +return 0 ; + } + len =check_leading_path (ce->name,ce_namelen (ce),0 );if (!len)return 0 ;
Signature · 1 line
gitgitgadget
PATCH v6 5/11 symlinks: do not include startup_info->original_cwd in dir removal
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
symlinks has a pair of schedule_dir_for_removal() and remove_scheduled_dirs() functions that ensure that directories made empty by removing other files also themselves get removed. However, we want to exclude startup_info->original_cwd and leave it around. This avoids the user getting confused by subsequent git commands (and non-git commands) that would otherwise report confusing messages about being unable to read the current working directory.
--- symlinks.c | 8 +++++++- t/t2501-cwd-empty.sh | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-)
symlinks.c +7 -1
@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len) {while (removal.len > new_len) { removal.buf[removal.len] ='\0' ; -if (rmdir (removal.buf)) +if ((startup_info->original_cwd && + !strcmp (removal.buf, startup_info->original_cwd)) || +rmdir (removal.buf))break ; do { removal.len--;
@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len) {int match_len, last_slash, i, previous_slash; +if (startup_info->original_cwd && + !strcmp (name, startup_info->original_cwd)) +return ;/* Do not remove the current working directory */ + match_len = last_slash = i =longest_path_match (name, len, removal.buf, removal.len, &previous_slash);
t/t2501-cwd-empty.sh +5 -5
@@ -109,7 +109,7 @@ test_required_dir_removal () { } test_expect_success 'checkout does not clean cwd incidentally' ' - test_incidental_dir_removal failure git checkout init + test_incidental_dir_removal success git checkout init ' test_expect_success 'checkout fails if cwd needs to be removed' '
@@ -117,7 +117,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' ' ' test_expect_success 'reset --hard does not clean cwd incidentally' ' - test_incidental_dir_removal failure git reset --hard init + test_incidental_dir_removal success git reset --hard init ' test_expect_success 'reset --hard fails if cwd needs to be removed' '
@@ -125,7 +125,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' ' ' test_expect_success 'merge does not clean cwd incidentally' ' - test_incidental_dir_removal failure git merge reverted + test_incidental_dir_removal success git merge reverted ' # This file uses some simple merges where
@@ -158,7 +158,7 @@ test_expect_success 'merge fails if cwd needs to be removed' ' ' test_expect_success 'cherry-pick does not clean cwd incidentally' ' - test_incidental_dir_removal failure git cherry-pick reverted + test_incidental_dir_removal success git cherry-pick reverted ' test_expect_success 'cherry-pick fails if cwd needs to be removed' '
@@ -174,7 +174,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' ' ' test_expect_success 'revert does not clean cwd incidentally' ' - test_incidental_dir_removal failure git revert HEAD + test_incidental_dir_removal success git revert HEAD ' test_expect_success 'revert fails if cwd needs to be removed' '
Signature · 1 line
gitgitgadget
PATCH v6 6/11 clean: do not attempt to remove startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
--- builtin/clean.c | 44 +++++++++++++++++++++++++++++++++++--------- t/t2501-cwd-empty.sh | 5 +++-- 2 files changed, 38 insertions(+), 11 deletions(-)
builtin/clean.c +35 -9
@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_("Skipping repository %s\n");static const char *msg_would_skip_git_dir =N_ ("Would skip repository %s\n" );static const char *msg_warn_remove_failed =N_ ("failed to remove %s" );static const char *msg_warn_lstat_failed =N_ ("could not lstat %s\n" ); +static const char *msg_skip_cwd =N_ ("Refusing to remove current working directory\n" ); +static const char *msg_would_skip_cwd =N_ ("Would refuse to remove current working directory\n" );enum color_clean {CLEAN_COLOR_RESET =0 ,
@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, {DIR *dir;struct strbuf quoted =STRBUF_INIT ; +struct strbuf realpath =STRBUF_INIT ; +struct strbuf real_ocwd =STRBUF_INIT ;struct dirent *e;int res =0 , ret =0 , gone =1 , original_len = path->len, len;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,strbuf_setlen (path, original_len);if (*dir_gone) { - res = dry_run ?0 :rmdir (path->buf); -if (!res) - *dir_gone =1 ; -else { -int saved_errno = errno; -quote_path (path->buf, prefix, "ed,0 ); - errno = saved_errno; -warning_errno (_ (msg_warn_remove_failed), quoted.buf); +/* +* Normalize path components in path->buf, e.g. change '\' to +* '/' on Windows. +*/ +strbuf_realpath (&realpath, path->buf,1 ); + +/* +* path and realpath are absolute; for comparison, we would +* like to transform startup_info->original_cwd to an absolute +* path too. +*/ +if (startup_info->original_cwd) +strbuf_realpath (&real_ocwd, + startup_info->original_cwd,1 ); + +if (!strbuf_cmp (&realpath, &real_ocwd)) { +printf ("%s" , dry_run ?_ (msg_would_skip_cwd) :_ (msg_skip_cwd)); *dir_gone =0 ; - ret =1 ; + }else { + res = dry_run ?0 :rmdir (path->buf); +if (!res) + *dir_gone =1 ; +else { +int saved_errno = errno; +quote_path (path->buf, prefix, "ed,0 ); + errno = saved_errno; +warning_errno (_ (msg_warn_remove_failed), quoted.buf); + *dir_gone =0 ; + ret =1 ; + } } }
@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,printf (dry_run ?_ (msg_would_remove) :_ (msg_remove), dels.items[i].string); } out: +strbuf_release (&realpath); +strbuf_release (&real_ocwd);strbuf_release ("ed);string_list_clear (&dels,0 );return ret;
t/t2501-cwd-empty.sh +3 -2
@@ -230,8 +230,9 @@ test_incidental_untracked_dir_removal () { } test_expect_success 'clean does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal failure \ - git -C .. clean -fd -e warnings . >warnings + test_incidental_untracked_dir_removal success \ + git -C .. clean -fd -e warnings . >warnings && + grep "Refusing to remove current working directory" warnings ' test_expect_success 'stash does not remove cwd incidentally' '
Signature · 1 line
gitgitgadget
PATCH v6 7/11 rebase: do not attempt to remove startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Since rebase spawns a checkout subprocess, make sure we run that from the startup_info->original_cwd directory, so that the checkout process knows to protect that directory.
--- sequencer.c | 2 ++ t/t2501-cwd-empty.sh | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-)
sequencer.c +2 -0
@@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts, cmd.git_cmd =1 ; +if (startup_info->original_cwd) + cmd.dir = startup_info->original_cwd;strvec_push (&cmd.args,"checkout" );strvec_push (&cmd.args, commit);strvec_pushf (&cmd.env_array,GIT_REFLOG_ACTION "=%s" , action);
t/t2501-cwd-empty.sh +2 -2
@@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' ' ' test_expect_success 'rebase does not clean cwd incidentally' ' - test_incidental_dir_removal failure git rebase reverted + test_incidental_dir_removal success git rebase reverted ' test_expect_success 'rebase fails if cwd needs to be removed' ' - test_required_dir_removal failure git rebase fd_conflict + test_required_dir_removal success git rebase fd_conflict ' test_expect_success 'revert does not clean cwd incidentally' '
Signature · 1 line
gitgitgadget
PATCH v6 8/11 stash: do not attempt to remove startup_info->original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Since stash spawns a clean subprocess, make sure we run that from the startup_info->original_cwd directory, so that the clean processs knows to protect that directory. Also, since the clean command might no longer run from the toplevel, pass the ':/' magic pathspec to ensure we still clean from the toplevel.
--- builtin/stash.c | 4 +++- t/t2501-cwd-empty.sh | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-)
builtin/stash.c +3 -1
@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int qstruct child_process cp =CHILD_PROCESS_INIT ; cp.git_cmd =1 ; +if (startup_info->original_cwd) + cp.dir = startup_info->original_cwd;strvec_pushl (&cp.args,"clean" ,"--force" , -"--quiet" ,"-d" ,NULL ); +"--quiet" ,"-d" ,":/" ,NULL );if (include_untracked ==INCLUDE_ALL_FILES )strvec_push (&cp.args,"-x" );if (run_command (&cp)) {
t/t2501-cwd-empty.sh +1 -1
@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' ' ' test_expect_success 'stash does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal failure \ + test_incidental_untracked_dir_removal success \ git stash --include-untracked '
Signature · 1 line
gitgitgadget
PATCH v6 9/11 dir: avoid incidentally removing the original_cwd in remove_path()
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
Modern git often tries to avoid leaving empty directories around when removing files. Originally, it did not bother. This behavior started with commit 80e21a9ed809 (merge-recursive::removeFile: remove empty directories, 2005-11-19), stating the reason simply as:
When the last file in a directory is removed as the result of a merge, try to rmdir the now-empty directory.
This was reimplemented in C and renamed to remove_path() in commit e1b3a2cad7 ("Build-in merge-recursive", 2008-02-07), but was still internal to merge-recursive.
This trend towards removing leading empty directories continued with commit d9b814cc97f1 (Add builtin "git rm" command, 2006-05-19), which stated the reasoning as:
The other question is what to do with leading directories. The old "git rm" script didn't do anything, which is somewhat inconsistent. This one will actually clean up directories that have become empty as a result of removing the last file, but maybe we want to have a flag to decide the behaviour?
remove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a function to remove as much as possible of a path, 2008-09-27), because it was noted that we had two separate implementations of the same idea AND both were buggy. It described the purpose of the function as
a function to remove as much as possible of a path
Why remove as much as possible? Well, at the time we probably would have said something like:
* removing leading directories makes things feel tidy
* removing leading directories doesn't hurt anything so long as they had no files in them.
But I don't believe those reasons hold when the empty directory happens to be the current working directory we inherited from our parent process. Leaving the parent process in a deleted directory can cause user confusion when subsequent processes fail: any git command, for example, will immediately fail with
fatal: Unable to read current working directory: No such file or directory
Other commands may similarly get confused. Modify remove_path() so that the empty leading directories it also deletes does not include the current working directory we inherited from our parent process. I have looked through every caller of remove_path() in the current codebase to make sure that all should take this change.
--- dir.c | 3 +++ dir.h | 6 +++++- t/t2501-cwd-empty.sh | 12 ++++-------- 3 files changed, 12 insertions(+), 9 deletions(-)
dir.c +3 -0
@@ -3327,6 +3327,9 @@ int remove_path(const char *name) slash = dirs + (slash - name);do { *slash ='\0' ; +if (startup_info->original_cwd && + !strcmp (startup_info->original_cwd, dirs)) +break ; }while (rmdir (dirs) ==0 && (slash =strrchr (dirs,'/' )));free (dirs); }
dir.h +5 -1
@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);*/ int remove_dir_recursively (struct strbuf *path,int flag); -/* tries to remove the path with empty directories along it, ignores ENOENT */ +/* +* Tries to remove the path, along with leading empty directories so long as +* those empty directories are not startup_info->original_cwd. Ignores +* ENOENT. +*/ int remove_path (const char *path);int fspathcmp (const char *a,const char *b);
t/t2501-cwd-empty.sh +4 -8
@@ -182,12 +182,12 @@ test_expect_success 'revert fails if cwd needs to be removed' ' ' test_expect_success 'rm does not clean cwd incidentally' ' - test_incidental_dir_removal failure git rm bar/baz.t + test_incidental_dir_removal success git rm bar/baz.t ' test_expect_success 'apply does not remove cwd incidentally' ' git diff HEAD HEAD~1 >patch && - test_incidental_dir_removal failure git apply ../patch + test_incidental_dir_removal success git apply ../patch ' test_incidental_untracked_dir_removal () {
@@ -271,12 +271,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet ) && test_path_is_missing a/b/c/tracked && - ## We would prefer if a/b was still present, though empty, since it - ## was the current working directory - #test_path_is_dir a/b - ## But the current behavior is that it not only deletes the directory - ## a/b as requested, but also goes and deletes a - test_path_is_missing a + test_path_is_missing a/b/c && + test_path_is_dir a/b ' test_expect_success 'git version continues working from a deleted dir' '
Signature · 1 line
gitgitgadget
PATCH v6 10/11 dir: new flag to remove_dir_recurse() to spare the original_cwd
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
remove_dir_recurse(), and its non-static wrapper called remove_dir_recursively(), both take flags for modifying its behavior. As with the previous commits, we would generally like to protect the original_cwd, but we want to forced user commands (e.g. 'git rm -rf ...') or other special cases to remove it. Add a flag for this purpose. After reading through every caller of remove_dir_recursively() in the current codebase, there was only one that should be adjusted and that one only in a very unusual circumstance. Add a pair of new testcases to highlight that very specific case involving submodules && --git-dir && --work-tree.
--- builtin/rm.c | 3 ++- dir.c | 12 +++++++++--- dir.h | 3 +++ t/t2501-cwd-empty.sh | 5 ----- 4 files changed, 14 insertions(+), 9 deletions(-)
builtin/rm.c +2 -1
@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)if (!index_only) {int removed =0 , gitmodules_modified =0 ;struct strbuf buf =STRBUF_INIT ; +int flag = force ?REMOVE_DIR_PURGE_ORIGINAL_CWD :0 ;for (i =0 ; i < list.nr; i++) {const char *path = list.entry[i].name;if (list.entry[i].is_submodule) {strbuf_reset (&buf);strbuf_addstr (&buf, path); -if (remove_dir_recursively (&buf,0 )) +if (remove_dir_recursively (&buf, flag))die (_ ("could not remove '%s'" ), path); removed =1 ;
dir.c +9 -3
@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)int ret =0 , original_len = path->len, len, kept_down =0 ;int only_empty = (flag &REMOVE_DIR_EMPTY_ONLY );int keep_toplevel = (flag &REMOVE_DIR_KEEP_TOPLEVEL ); +int purge_original_cwd = (flag &REMOVE_DIR_PURGE_ORIGINAL_CWD );struct object_id submodule_head; if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)closedir (dir);strbuf_setlen (path, original_len); -if (!ret && !keep_toplevel && !kept_down) - ret = (!rmdir (path->buf) || errno ==ENOENT ) ?0 :-1 ; -else if (kept_up) +if (!ret && !keep_toplevel && !kept_down) { +if (!purge_original_cwd && + startup_info->original_cwd && + !strcmp (startup_info->original_cwd, path->buf)) + ret =-1 ;/* Do not remove current working directory */ +else + ret = (!rmdir (path->buf) || errno ==ENOENT ) ?0 :-1 ; + }else if (kept_up)/* * report the uplevel that it is not an error that we * did not rmdir() our directory.
dir.h +3 -0
@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);/* Remove the contents of path, but leave path itself. */ #define REMOVE_DIR_KEEP_TOPLEVEL 04 +/* Remove the_original_cwd too */ +#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08 +/* * Remove path and its contents, recursively. flags is a combination * of the above REMOVE_DIR_* constants. Return 0 on success.
t/t2501-cwd-empty.sh +0 -5
@@ -291,11 +291,6 @@ test_submodule_removal () { test_status= test "$path_status" = dir && test_status=test_must_fail - # Actually, while path_status=dir && test_status=test_must_fail - # reflect our desired behavior, current behavior is: - path_status=missing - test_status= - test_when_finished "git reset --hard HEAD~1" && test_when_finished "rm -rf .git/modules/my_submodule" &&
Signature · 1 line
gitgitgadget
PATCH v6 11/11 t2501: simplify the tests since we can now assume desired behavior
In reply to Elijah Newren via GitGitGadgetFrom: Elijah Newren <[email protected]>
We no longer are dealing with a mixture of previous and desired behavior, so simplify the tests a bit.
--- t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------ 1 file changed, 36 insertions(+), 87 deletions(-)
t/t2501-cwd-empty.sh +36 -87
@@ -32,9 +32,6 @@ test_expect_success setup ' ' test_incidental_dir_removal () { - works=$1 && - shift && - test_when_finished "git reset --hard" && git checkout foo/bar/baz^{commit} &&
@@ -44,88 +41,57 @@ test_incidental_dir_removal () { cd foo && "$@" && - # Although we want pwd & git status to pass, test for existing - # rather than desired behavior. - if test "$works" = "success" - then - test-tool getcwd && - git status --porcelain - else - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Make sure foo still exists, and commands needing it work + test-tool getcwd && + git status --porcelain ) && test_path_is_missing foo/bar/baz && test_path_is_missing foo/bar && - # Although we want dir to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir foo - else - test_path_is_missing foo - fi + test_path_is_dir foo } test_required_dir_removal () { - works=$1 && - shift && - git checkout df_conflict^{commit} && test_when_finished "git clean -fdx" && ( cd dirORfile && - # We'd like for the command to fail (much as it would if there - # was an untracked file there), and for the index and worktree - # to be left clean with pwd and git status working afterwards. - # But test for existing rather than desired behavior. - if test "$works" = "success" - then - test_must_fail "$@" 2>../error && - grep "Refusing to remove.*current working directory" ../error && - - git diff --exit-code HEAD && - - test-tool getcwd && - git status --porcelain - else - "$@" && - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Ensure command refuses to run + test_must_fail "$@" 2>../error && + grep "Refusing to remove.*current working directory" ../error && + + # ...and that the index and working tree are left clean + git diff --exit-code HEAD && + + # Ensure that getcwd and git status do not error out (which + # they might if the current working directory had been removed) + test-tool getcwd && + git status --porcelain ) && - # Although we want dirORfile to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir dirORfile - else - test_path_is_file dirORfile - fi + test_path_is_dir dirORfile } test_expect_success 'checkout does not clean cwd incidentally' ' - test_incidental_dir_removal success git checkout init + test_incidental_dir_removal git checkout init ' test_expect_success 'checkout fails if cwd needs to be removed' ' - test_required_dir_removal success git checkout fd_conflict + test_required_dir_removal git checkout fd_conflict ' test_expect_success 'reset --hard does not clean cwd incidentally' ' - test_incidental_dir_removal success git reset --hard init + test_incidental_dir_removal git reset --hard init ' test_expect_success 'reset --hard fails if cwd needs to be removed' ' - test_required_dir_removal success git reset --hard fd_conflict + test_required_dir_removal git reset --hard fd_conflict ' test_expect_success 'merge does not clean cwd incidentally' ' - test_incidental_dir_removal success git merge reverted + test_incidental_dir_removal git merge reverted ' # This file uses some simple merges where
@@ -154,46 +120,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' GIT_TEST_MERGE_ALGORITHM=ort test_expect_success 'merge fails if cwd needs to be removed' ' - test_required_dir_removal success git merge fd_conflict + test_required_dir_removal git merge fd_conflict ' test_expect_success 'cherry-pick does not clean cwd incidentally' ' - test_incidental_dir_removal success git cherry-pick reverted + test_incidental_dir_removal git cherry-pick reverted ' test_expect_success 'cherry-pick fails if cwd needs to be removed' ' - test_required_dir_removal success git cherry-pick fd_conflict + test_required_dir_removal git cherry-pick fd_conflict ' test_expect_success 'rebase does not clean cwd incidentally' ' - test_incidental_dir_removal success git rebase reverted + test_incidental_dir_removal git rebase reverted ' test_expect_success 'rebase fails if cwd needs to be removed' ' - test_required_dir_removal success git rebase fd_conflict + test_required_dir_removal git rebase fd_conflict ' test_expect_success 'revert does not clean cwd incidentally' ' - test_incidental_dir_removal success git revert HEAD + test_incidental_dir_removal git revert HEAD ' test_expect_success 'revert fails if cwd needs to be removed' ' - test_required_dir_removal success git revert undo_fd_conflict + test_required_dir_removal git revert undo_fd_conflict ' test_expect_success 'rm does not clean cwd incidentally' ' - test_incidental_dir_removal success git rm bar/baz.t + test_incidental_dir_removal git rm bar/baz.t ' test_expect_success 'apply does not remove cwd incidentally' ' git diff HEAD HEAD~1 >patch && - test_incidental_dir_removal success git apply ../patch + test_incidental_dir_removal git apply ../patch ' test_incidental_untracked_dir_removal () { - works=$1 && - shift && - test_when_finished "git reset --hard" && git checkout foo/bar/baz^{commit} &&
@@ -205,38 +168,24 @@ test_incidental_untracked_dir_removal () { cd untracked && "$@" && - # Although we want pwd & git status to pass, test for existing - # rather than desired behavior. - if test "$works" = "success" - then - test-tool getcwd && - git status --porcelain - else - ! test-tool getcwd && - test_might_fail git status --porcelain - fi + # Make sure untracked still exists, and commands needing it work + test-tool getcwd && + git status --porcelain ) && test_path_is_missing empty && test_path_is_missing untracked/random && - # Although we want dir to be present, test for existing rather - # than desired behavior. - if test "$works" = "success" - then - test_path_is_dir untracked - else - test_path_is_missing untracked - fi + test_path_is_dir untracked } test_expect_success 'clean does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal success \ + test_incidental_untracked_dir_removal \ git -C .. clean -fd -e warnings . >warnings && grep "Refusing to remove current working directory" warnings ' test_expect_success 'stash does not remove cwd incidentally' ' - test_incidental_untracked_dir_removal success \ + test_incidental_untracked_dir_removal \ git stash --include-untracked '
Signature · 1 line
gitgitgadget
Hi Elijah
Quoted 14 lines · Elijah Newren via GitGitGadget
Traditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the
original_cwdas well.Peff and Junio provided some good pros/cons, if it helps:
Quoted 8 lines
* Pros: Peff (original suggester of the idea)[2], and Junio[3]
* Cons: Peff [2, again -- see the "P.S."], and Junio[4][1] well, with a few exceptions; see
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/ [4]
https://lore.kernel.org/git/[email protected]/
Thanks for working on this, I'm sorry I haven't had time to take a proper look at it but I think it is a good idea. I did notice that you're using ":/" in patch 8 and wondered what happens if some runs 'git --literal-pathspecs stash' but I haven't looked properly.
Best Wishes
Phillip
Quoted 1,108 lines
Changes since v2:
* the series is now only about the working tree. So if the original cwd is outside the worktree (or we're in a bare repo), then the new code is a no-op.
* fixed ugly earlydie()possibility (usesstrbuf_getcwd()instead ofxgetcwd())* modified the initial tests to show both expected and desired behavior. subsequent patches fix the tests. One new patch added at the end which simplifies the tests to only check for desired behavior.
* NULLifystartup_info->original_cwdwhen it matches the toplevel worktree; that is already protected and we don't need secondary protection for it. This simplified some other codepaths so we don't have to check forstartup_info->original_cwd== "".
* clarified some commit messagesChanges since v1:
* clarified multiple commit messages
* renamedthe_cwdtostartup_info->original_cwdto make it clearer that it's our parent process'es cwd that really matters, which we inherited at program startup. Also pulls it out of the global namespace.
* Normalize the path forstartup_info->original_cwd, and ensure that it's actually the original cwd even if -C is passed to git.
* small code cleanups suggested by René and Ævar
* split the final patch, which got the most comments into two, one for each function being modified; significantly extending the first of the two commit messages with a lot of history
* no longer has a content conflict with so/stash-staged
* add another value for the flags parameter thatremove_dir_recursively()takes so that it can opt into either the old or the new behavior. Use that for the one special corner case I could find where it matters, and add a few tests around it to highlight the utility of the flag.Elijah Newren (11):
t2501: add various tests for removing the current working directory
setup: introducestartup_info->original_cwd
unpack-trees: refuse to removestartup_info->original_cwd
unpack-trees: add special cwd handling
symlinks: do not includestartup_info->original_cwdin dir removal
clean: do not attempt to removestartup_info->original_cwd
rebase: do not attempt to removestartup_info->original_cwd
stash: do not attempt to removestartup_info->original_cwd
dir: avoid incidentally removing theoriginal_cwdinremove_path()
dir: new flag toremove_dir_recurse()to spare theoriginal_cwd
t2501: simplify the tests since we can now assume desired behaviorbuiltin/clean.c | 44 +++++-- builtin/rm.c | 3 +- builtin/stash.c | 5 +- cache.h | 2 + common-main.c | 4 + dir.c | 15 ++- dir.h | 9 +- sequencer.c | 3 + setup.c | 65 +++++++++++ symlinks.c | 8 +- t/t2501-cwd-empty.sh | 268 +++++++++++++++++++++++++++++++++++++++++++ unpack-trees.c | 30 ++++- unpack-trees.h | 1 + 13 files changed, 435 insertions(+), 22 deletions(-)create mode 100755
t/t2501-cwd-empty.shbase-commit:
88d915a634b449147855041d44875322de2b286d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v3
Pull-Request: https://github.com/git/git/pull/1140Range-diff vs v2:
1: 38a120f5c03 ! 1: 4b0044656b0 t2501: add various tests for removing the current working directory @@ Metadata ## Commit message ## t2501: add various tests for removing the current working directory - Numerous commands will remove empty working directories, especially if - they are in the way of placing needed files. That is normally fine, but - removing the current working directory can cause confusion for the user - when they run subsequent commands. For example, after one git process - has removed the current working directory, git status/log/diff will all - abort with the message: + Numerous commands will remove directories left empty as a "convenience" + after removing files within them. That is normally fine, but removing + the current working directory can be rather inconvenient since it can + cause confusion for the user when they run subsequent commands. For + example, after one git process has removed the current working + directory, git status/log/diff will all abort with the message: fatal: Unable to read current working directory: No such file or directory + We also have code paths that, when a file needs to be placed where a + directory is (due to e.g. checkout, merge, reset, whatever), will check + if this is okay and error out if not. These rules include: + * all tracked files under that directory are intended to be removed by + the operation + * none of the tracked files under that directory have uncommitted + modification + * there are no untracked files under that directory + However, if we end up remove the current working directory, we can cause + user confusion when they run subsequent commands, so we would prefer if + there was a fourth rule added to this list: avoid removing the current + working directory. + Since there are several code paths that can result in the current working directory being removed, add several tests of various different - codepaths that check for the behavior we would instead like to see. - This include a number of new error messages that we will be adding in - subsequent commits as we implement the desired checks. + codepaths. To make it clearer what the difference between the current + behavior and the behavior at the end of the series, code both of them + into the tests and have the appropriate behavior be selected by a flag. + Subsequent commits will toggle the flag from current to desired + behavior. + + Also add a few tests suggested during the review of earlier rounds of + this patch series. Signed-off-by: Elijah Newren <[email protected]> @@ t/t2501-cwd-empty.sh (new) + +test_expect_success setup ' + test_commit init && -+ mkdir subdir && -+ test_commit subdir/file && + + git branch fd_conflict && + @@ t/t2501-cwd-empty.sh (new) + git tag reverted && + + git checkout fd_conflict && -+ git rm subdir/file.t && ++ mkdir dirORfile && ++ test_commit dirORfile/foo && ++ ++ git rm -r dirORfile && + echo not-a-directory >dirORfile && + git add dirORfile && -+ git commit -m dirORfile ++ git commit -m dirORfile && ++ ++ git switch -c df_conflict HEAD~1 && ++ test_commit random_file && ++ ++ git switch -c undo_fd_conflict fd_conflict && ++ git revert HEAD +' + -+test_expect_failure 'checkout does not clean cwd incidentally' ' -+ git checkout foo/bar/baz && ++test_incidental_dir_removal () { ++ works=$1 && ++ shift && ++ ++ test_when_finished "git reset --hard" && ++ ++ git checkout foo/bar/baz^{commit} && + test_path_is_dir foo/bar && + + ( + cd foo && -+ git checkout init && -+ cd .. ++ "$@" && ++ ++ # Although we want pwd & git status to pass, test for existing ++ # rather than desired behavior. ++ if [[ $works == "success" ]]; then ++ pwd -P && ++ git status --porcelain ++ else ++ ! pwd -P && ++ test_might_fail git status --porcelain ++ fi + ) && + test_path_is_missing foo/bar/baz && + test_path_is_missing foo/bar && -+ test_path_is_dir foo -+' + -+test_expect_failure 'checkout fails if cwd needs to be removed' ' -+ git checkout foo/bar/baz && ++ # Although we want dir to be present, test for existing rather ++ # than desired behavior. ++ if [[ $works == "success" ]]; then ++ test_path_is_dir foo ++ else ++ test_path_is_missing foo ++ fi ++} ++ ++test_required_dir_removal () { ++ works=$1 && ++ shift && ++ ++ git checkout df_conflict^{commit} && + test_when_finished "git clean -fdx" && + -+ mkdir dirORfile && + ( + cd dirORfile && + -+ test_must_fail git checkout fd_conflict 2>../error && -+ grep "Refusing to remove the current working directory" ../error ++ # We'd like for the command to fail (much as it would if there ++ # was an untracked file there), and for pwd & git status to ++ # succeed afterwards. But test for existing rather than ++ # desired behavior. ++ if [[ $works == "success" ]]; then ++ test_must_fail "$@" 2>../error && ++ grep "Refusing to remove.*current working directory" ../error && ++ pwd -P && ++ git status --porcelain ++ else ++ "$@" && ++ ! pwd -P && ++ test_might_fail git status --porcelain ++ fi + ) && + -+ test_path_is_dir dirORfile ++ # Although we want dirORfile to be present, test for existing rather ++ # than desired behavior. ++ if [[ $works == "success" ]]; then ++ test_path_is_dir dirORfile ++ else ++ test_path_is_file dirORfile ++ fi ++} ++ ++test_expect_success 'checkout does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git checkout init +' + -+test_expect_failure 'reset --hard does not clean cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_path_is_dir foo/bar && ++test_expect_success 'checkout fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git checkout fd_conflict ++' + -+ ( -+ cd foo && -+ git reset --hard init && -+ cd .. -+ ) && -+ test_path_is_missing foo/bar/baz && -+ test_path_is_missing foo/bar && -+ test_path_is_dir foo ++test_expect_success 'reset --hard does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git reset --hard init ++' ++ ++test_expect_success 'reset --hard fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git reset --hard fd_conflict ++' ++ ++test_expect_success 'merge does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git merge reverted +' + -+test_expect_failure 'reset --hard fails if cwd needs to be removed' ' ++# This file uses some simple merges where ++# Base: 'dirORfile/' exists ++# Side1: random other file changed ++# Side2: 'dirORfile/' removed, 'dirORfile' added ++# this should resolve cleanly, but merge-recursive throws merge conflicts ++# because it's dumb. Add a special test for checking merge-recursive (and ++# merge-ort), then after this just hard require ort for all remaining tests. ++# ++test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' ' + git checkout foo/bar/baz && + test_when_finished "git clean -fdx" && + @@ t/t2501-cwd-empty.sh (new) + ( + cd dirORfile && + -+ test_must_fail git reset --hard fd_conflict 2>../error && -+ grep "Refusing to remove.*the current working directory" ../error ++ # We would rather this failed, but we test for existing ++ # rather than desired behavior ++ git merge fd_conflict 2>../error + ) && + -+ test_path_is_dir dirORfile ++ ## Here is the behavior we would rather have: ++ #test_path_is_dir dirORfile && ++ #grep "Refusing to remove the current working directory" error ++ ## But instead we test for existing behavior ++ test_path_is_file dirORfile && ++ test_must_be_empty error +' + -+test_expect_failure 'merge does not remove cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++GIT_TEST_MERGE_ALGORITHM=ort + -+ ( -+ cd subdir && -+ git merge fd_conflict -+ ) && ++test_expect_success 'merge fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git merge fd_conflict ++' + -+ test_path_is_missing subdir/file.t && -+ test_path_is_dir subdir ++test_expect_success 'cherry-pick does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git cherry-pick reverted +' + -+test_expect_failure 'merge fails if cwd needs to be removed' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++test_expect_success 'cherry-pick fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git cherry-pick fd_conflict ++' + -+ mkdir dirORfile && -+ ( -+ cd dirORfile && -+ test_must_fail git merge fd_conflict 2>../error && -+ grep "Refusing to remove the current working directory" ../error -+ ) && ++test_expect_success 'rebase does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git rebase reverted ++' + -+ test_path_is_dir dirORfile ++test_expect_success 'rebase fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git rebase fd_conflict +' + -+test_expect_failure 'cherry-pick does not remove cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++test_expect_success 'revert does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git revert HEAD ++' + -+ ( -+ cd subdir && -+ git cherry-pick fd_conflict -+ ) && ++test_expect_success 'revert fails if cwd needs to be removed' ' ++ test_required_dir_removal failure git revert undo_fd_conflict ++' + -+ test_path_is_missing subdir/file.t && -+ test_path_is_dir subdir ++test_expect_success 'rm does not clean cwd incidentally' ' ++ test_incidental_dir_removal failure git rm bar/baz.t +' + -+test_expect_failure 'cherry-pick fails if cwd needs to be removed' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++test_expect_success 'apply does not remove cwd incidentally' ' ++ git diff HEAD HEAD~1 >patch && ++ test_incidental_dir_removal failure git apply ../patch ++' + -+ mkdir dirORfile && -+ ( -+ cd dirORfile && -+ test_must_fail git cherry-pick fd_conflict 2>../error && -+ grep "Refusing to remove the current working directory" ../error -+ ) && ++test_incidental_untracked_dir_removal () { ++ works=$1 && ++ shift && + -+ test_path_is_dir dirORfile -+' ++ test_when_finished "git reset --hard" && + -+test_expect_failure 'rebase does not remove cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++ git checkout foo/bar/baz^{commit} && ++ mkdir -p untracked && ++ mkdir empty ++ >untracked/random && + + ( -+ cd subdir && -+ git rebase foo/bar/baz fd_conflict ++ cd untracked && ++ "$@" && ++ ++ # Although we want pwd & git status to pass, test for existing ++ # rather than desired behavior. ++ if [[ $works == "success" ]]; then ++ pwd -P && ++ git status --porcelain ++ else ++ ! pwd -P && ++ test_might_fail git status --porcelain ++ fi + ) && ++ test_path_is_missing empty && ++ test_path_is_missing untracked/random && + -+ test_path_is_missing subdir/file.t && -+ test_path_is_dir subdir ++ # Although we want dir to be present, test for existing rather ++ # than desired behavior. ++ if [[ $works == "success" ]]; then ++ test_path_is_dir untracked ++ else ++ test_path_is_missing untracked ++ fi ++} ++ ++test_expect_success 'clean does not remove cwd incidentally' ' ++ test_incidental_untracked_dir_removal failure \ ++ git -C .. clean -fd -e warnings . >warnings +' + -+test_expect_failure 'rebase fails if cwd needs to be removed' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && -+ -+ mkdir dirORfile && -+ ( -+ cd dirORfile && -+ test_must_fail git rebase foo/bar/baz fd_conflict 2>../error && -+ grep "Refusing to remove the current working directory" ../error -+ ) && -+ -+ test_path_is_dir dirORfile ++test_expect_success 'stash does not remove cwd incidentally' ' ++ test_incidental_untracked_dir_removal failure \ ++ git stash --include-untracked +' + -+test_expect_failure 'revert does not remove cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++test_expect_success '`rm -rf dir` only removes a subset of dir' ' ++ test_when_finished "rm -rf a/" && ++ ++ mkdir -p a/b/c && ++ >a/b/c/untracked && ++ >a/b/c/tracked && ++ git add a/b/c/tracked && + + ( -+ cd subdir && -+ git revert subdir/file ++ cd a/b && ++ git rm -rf ../b + ) && + -+ test_path_is_missing subdir/file.t && -+ test_path_is_dir subdir ++ test_path_is_dir a/b && ++ test_path_is_missing a/b/c/tracked && ++ test_path_is_file a/b/c/untracked +' + -+test_expect_failure 'revert fails if cwd needs to be removed' ' -+ git checkout fd_conflict && -+ git revert HEAD && -+ test_when_finished "git clean -fdx" && ++test_expect_success '`rm -rf dir` even with only tracked files will remove something else' ' ++ test_when_finished "rm -rf a/" && ++ ++ mkdir -p a/b/c && ++ >a/b/c/tracked && ++ git add a/b/c/tracked && + -+ mkdir dirORfile && + ( -+ cd dirORfile && -+ test_must_fail git revert HEAD 2>../error && -+ grep "Refusing to remove the current working directory" ../error ++ cd a/b && ++ git rm -rf ../b + ) && + -+ test_path_is_dir dirORfile ++ test_path_is_missing a/b/c/tracked && ++ ## We would prefer if a/b was still present, though empty, since it ++ ## was the current working directory ++ #test_path_is_dir a/b ++ ## But the current behavior is that it not only deletes the directory ++ ## a/b as requested, but also goes and deletes a ++ test_path_is_missing a +' + -+test_expect_failure 'rm does not remove cwd incidentally' ' -+ test_when_finished "git reset --hard" && -+ git checkout foo/bar/baz && -+ ++test_expect_success 'git version continues working from a deleted dir' ' ++ mkdir tmp && + ( -+ cd foo && -+ git rm bar/baz.t -+ ) && -+ -+ test_path_is_missing foo/bar/baz && -+ test_path_is_missing foo/bar && -+ test_path_is_dir foo ++ cd tmp && ++ rm -rf ../tmp && ++ git version ++ ) +' + -+test_expect_failure 'apply does not remove cwd incidentally' ' -+ test_when_finished "git reset --hard" && -+ git checkout foo/bar/baz && ++test_submodule_removal () { ++ path_status=$1 && ++ shift && + -+ ( -+ cd subdir && -+ git diff subdir/file init | git apply -+ ) && ++ test_status= ++ test $path_status = dir && test_status=test_must_fail + -+ test_path_is_missing subdir/file.t && -+ test_path_is_dir subdir -+' ++ # Actually, while path_status == dir && test_status=test_must_fail ++ # reflect our desired behavior, current behavior is: ++ path_status=missing ++ test_status= ++ ++ test_when_finished "git reset --hard HEAD~1" && ++ test_when_finished "rm -rf .git/modules/my_submodule" && + -+test_expect_failure 'clean does not remove cwd incidentally' ' + git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && + -+ mkdir empty && -+ mkdir untracked && -+ >untracked/random && ++ git init my_submodule && ++ touch my_submodule/file && ++ git -C my_submodule add file && ++ git -C my_submodule commit -m "initial commit" && ++ git submodule add ./my_submodule && ++ git commit -m "Add the submodule" && ++ + ( -+ cd untracked && -+ git clean -fd -e warnings :/ >../warnings && -+ grep "Refusing to remove current working directory" ../warnings ++ cd my_submodule && ++ $test_status "$@" + ) && + -+ test_path_is_missing empty && -+ test_path_is_missing untracked/random && -+ test_path_is_dir untracked -+' ++ test_path_is_${path_status} my_submodule ++} + -+test_expect_failure 'stash does not remove cwd incidentally' ' -+ git checkout foo/bar/baz && -+ test_when_finished "git clean -fdx" && ++test_expect_success 'rm -r with -C leaves submodule if cwd inside' ' ++ test_submodule_removal dir git -C .. rm -r my_submodule/ ++' + -+ mkdir untracked && -+ >untracked/random && -+ ( -+ cd untracked && -+ git stash --include-untracked && -+ git status -+ ) && ++test_expect_success 'rm -r leaves submodule if cwd inside' ' ++ test_submodule_removal dir \ ++ git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/ ++' + -+ test_path_is_missing untracked/random && -+ test_path_is_dir untracked ++test_expect_success 'rm -rf removes submodule even if cwd inside' ' ++ test_submodule_removal missing \ ++ git --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/ +' + +test_done 2: f6129a8ac9c ! 2: 200ddece05d setup: introduce startup_info->original_cwd @@ cache.h: void overlay_tree_on_index(struct index_state *istate, + const char *original_cwd; }; extern struct startup_info *startup_info; ++extern const char *tmp_original_cwd; + /* merge.c */ + struct commit_list; - ## git.c ## -@@ git.c: int cmd_main(int argc, const char **argv) + ## common-main.c ## +@@ common-main.c: static void restore_sigpipe_to_default(void) + int main(int argc, const char **argv) + { + int result; ++ struct strbuf tmp = STRBUF_INIT; + + trace2_initialize_clock(); - trace_command_performance(argv); +@@ common-main.c: int main(int argc, const char **argv) + trace2_cmd_start(argv); + trace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP); -+ startup_info->original_cwd = xgetcwd(); ++ if (!strbuf_getcwd(&tmp)) ++ tmp_original_cwd = strbuf_detach(&tmp, NULL); + - /* - * "git-xxxx" is the same as "git xxxx", but we obviously: - * + result = cmd_main(argc, argv); + + trace2_cmd_exit(result); ## setup.c ## +@@ setup.c: static int work_tree_config_is_bogus; + + static struct startup_info the_startup_info; + struct startup_info *startup_info = &the_startup_info; ++const char *tmp_original_cwd; + + /* + * The input parameter must contain an absolute path, and it must already be @@ setup.c: void setup_work_tree(void) initialized = 1; } @@ setup.c: void setup_work_tree(void) + const char *worktree = NULL; + int offset = -1; + -+ /* -+ * startup_info->original_cwd wass set early on in cmd_main(), unless -+ * we're an auxiliary tool like git-remote-http or test-tool. -+ */ -+ if (!startup_info->original_cwd) ++ if (!tmp_original_cwd) + return; + + /* + * startup_info->original_cwd points to the current working + * directory we inherited from our parent process, which is a -+ * directory we want to avoid incidentally removing. ++ * directory we want to avoid removing. + * + * For convience, we would like to have the path relative to the + * worktree instead of an absolute path. @@ setup.c: void setup_work_tree(void) + */ + + /* Normalize the directory */ -+ strbuf_realpath(&tmp, startup_info->original_cwd, 1); -+ free((char*)startup_info->original_cwd); ++ strbuf_realpath(&tmp, tmp_original_cwd, 1); ++ free((char*)tmp_original_cwd); ++ tmp_original_cwd = NULL; + startup_info->original_cwd = strbuf_detach(&tmp, NULL); + -+ /* Find out if this is in the worktree */ ++ /* ++ * Get our worktree; we only protect the current working directory ++ * if it's in the worktree. ++ */ + worktree = get_git_work_tree(); -+ if (worktree) -+ offset = dir_inside_of(startup_info->original_cwd, worktree); ++ if (!worktree) ++ goto no_prevention_needed; ++ ++ offset = dir_inside_of(startup_info->original_cwd, worktree); + if (offset >= 0) { + /* ++ * If startup_info->original_cwd == worktree, that is already ++ * protected and we don't need original_cwd as a secondary ++ * protection measure. ++ */ ++ if (!*(startup_info->original_cwd + offset)) ++ goto no_prevention_needed; ++ ++ /* + * original_cwd was inside worktree; precompose it just as + * we do prefix so that built up paths will match + */ @@ setup.c: void setup_work_tree(void) + precompose_string_if_needed(startup_info->original_cwd + + offset); + } ++ return; ++ ++no_prevention_needed: ++ free((char*)startup_info->original_cwd); ++ startup_info->original_cwd = NULL; +} + static int read_worktree_config(const char *var, const char *value, void *vdata) 3: e74975e83cc ! 3: 68ae90546fe unpack-trees: refuse to remove startup_info->original_cwd @@ Commit message Signed-off-by: Elijah Newren <[email protected]> ## t/t2501-cwd-empty.sh ## -@@ t/t2501-cwd-empty.sh: test_expect_failure 'checkout does not clean cwd incidentally' ' - test_path_is_dir foo +@@ t/t2501-cwd-empty.sh: test_expect_success 'checkout does not clean cwd incidentally' ' ' --test_expect_failure 'checkout fails if cwd needs to be removed' ' -+test_expect_success 'checkout fails if cwd needs to be removed' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'checkout fails if cwd needs to be removed' ' +- test_required_dir_removal failure git checkout fd_conflict ++ test_required_dir_removal success git checkout fd_conflict + ' + + test_expect_success 'reset --hard does not clean cwd incidentally' ' +@@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' + ( + cd dirORfile && + +- # We would rather this failed, but we test for existing +- # rather than desired behavior +- git merge fd_conflict 2>../error ++ test_must_fail git merge fd_conflict 2>../error + ) && -@@ t/t2501-cwd-empty.sh: test_expect_failure 'merge does not remove cwd incidentally' ' - test_path_is_dir subdir +- ## Here is the behavior we would rather have: +- #test_path_is_dir dirORfile && +- #grep "Refusing to remove the current working directory" error +- ## But instead we test for existing behavior +- test_path_is_file dirORfile && +- test_must_be_empty error ++ test_path_is_dir dirORfile && ++ grep "Refusing to remove the current working directory" error ' --test_expect_failure 'merge fails if cwd needs to be removed' ' -+test_expect_success 'merge fails if cwd needs to be removed' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + GIT_TEST_MERGE_ALGORITHM=ort -@@ t/t2501-cwd-empty.sh: test_expect_failure 'cherry-pick does not remove cwd incidentally' ' - test_path_is_dir subdir + test_expect_success 'merge fails if cwd needs to be removed' ' +- test_required_dir_removal failure git merge fd_conflict ++ test_required_dir_removal success git merge fd_conflict ' --test_expect_failure 'cherry-pick fails if cwd needs to be removed' ' -+test_expect_success 'cherry-pick fails if cwd needs to be removed' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'cherry-pick does not clean cwd incidentally' ' +@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick does not clean cwd incidentally' ' + ' -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rebase does not remove cwd incidentally' ' - test_path_is_dir subdir + test_expect_success 'cherry-pick fails if cwd needs to be removed' ' +- test_required_dir_removal failure git cherry-pick fd_conflict ++ test_required_dir_removal success git cherry-pick fd_conflict ' --test_expect_failure 'rebase fails if cwd needs to be removed' ' -+test_expect_success 'rebase fails if cwd needs to be removed' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'rebase does not clean cwd incidentally' ' +@@ t/t2501-cwd-empty.sh: test_expect_success 'revert does not clean cwd incidentally' ' + ' -@@ t/t2501-cwd-empty.sh: test_expect_failure 'revert does not remove cwd incidentally' ' - test_path_is_dir subdir + test_expect_success 'revert fails if cwd needs to be removed' ' +- test_required_dir_removal failure git revert undo_fd_conflict ++ test_required_dir_removal success git revert undo_fd_conflict ' --test_expect_failure 'revert fails if cwd needs to be removed' ' -+test_expect_success 'revert fails if cwd needs to be removed' ' - git checkout fd_conflict && - git revert HEAD && - test_when_finished "git clean -fdx" && + test_expect_success 'rm does not clean cwd incidentally' ' ## unpack-trees.c ## @@ unpack-trees.c: static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = { 4: e06806e3a32 ! 4: 1bb8905900c unpack-trees: add special cwd handling @@ Commit message Signed-off-by: Elijah Newren <[email protected]> ## t/t2501-cwd-empty.sh ## -@@ t/t2501-cwd-empty.sh: test_expect_failure 'reset --hard does not clean cwd incidentally' ' - test_path_is_dir foo +@@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard does not clean cwd incidentally' ' ' --test_expect_failure 'reset --hard fails if cwd needs to be removed' ' -+test_expect_success 'reset --hard fails if cwd needs to be removed' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'reset --hard fails if cwd needs to be removed' ' +- test_required_dir_removal failure git reset --hard fd_conflict ++ test_required_dir_removal success git reset --hard fd_conflict + ' + test_expect_success 'merge does not clean cwd incidentally' ' ## unpack-trees.c ## @@ unpack-trees.c: static int verify_absent_1(const struct cache_entry *ce, 5: 46728f74ea1 ! 5: 8a33d74e7cf symlinks: do not include startup_info->original_cwd in dir removal @@ symlinks.c: void schedule_dir_for_removal(const char *name, int len) &previous_slash); ## t/t2501-cwd-empty.sh ## -@@ t/t2501-cwd-empty.sh: test_expect_success setup ' - git commit -m dirORfile - ' +@@ t/t2501-cwd-empty.sh: test_required_dir_removal () { + } --test_expect_failure 'checkout does not clean cwd incidentally' ' -+test_expect_success 'checkout does not clean cwd incidentally' ' - git checkout foo/bar/baz && - test_path_is_dir foo/bar && + test_expect_success 'checkout does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git checkout init ++ test_incidental_dir_removal success git checkout init + ' + test_expect_success 'checkout fails if cwd needs to be removed' ' @@ t/t2501-cwd-empty.sh: test_expect_success 'checkout fails if cwd needs to be removed' ' - test_path_is_dir dirORfile ' --test_expect_failure 'reset --hard does not clean cwd incidentally' ' -+test_expect_success 'reset --hard does not clean cwd incidentally' ' - git checkout foo/bar/baz && - test_path_is_dir foo/bar && + test_expect_success 'reset --hard does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git reset --hard init ++ test_incidental_dir_removal success git reset --hard init + ' + test_expect_success 'reset --hard fails if cwd needs to be removed' ' @@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard fails if cwd needs to be removed' ' - test_path_is_dir dirORfile ' --test_expect_failure 'merge does not remove cwd incidentally' ' -+test_expect_success 'merge does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'merge does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git merge reverted ++ test_incidental_dir_removal success git merge reverted + ' + # This file uses some simple merges where @@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed' ' - test_path_is_dir dirORfile ' --test_expect_failure 'cherry-pick does not remove cwd incidentally' ' -+test_expect_success 'cherry-pick does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && - -@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick fails if cwd needs to be removed' ' - test_path_is_dir dirORfile + test_expect_success 'cherry-pick does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git cherry-pick reverted ++ test_incidental_dir_removal success git cherry-pick reverted ' --test_expect_failure 'rebase does not remove cwd incidentally' ' -+test_expect_success 'rebase does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && - + test_expect_success 'cherry-pick fails if cwd needs to be removed' ' @@ t/t2501-cwd-empty.sh: test_expect_success 'rebase fails if cwd needs to be removed' ' - test_path_is_dir dirORfile ' --test_expect_failure 'revert does not remove cwd incidentally' ' -+test_expect_success 'revert does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'revert does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git revert HEAD ++ test_incidental_dir_removal success git revert HEAD + ' + test_expect_success 'revert fails if cwd needs to be removed' ' 6: 01ce9444dae ! 6: 11e4ec881bb clean: do not attempt to remove startup_info->original_cwd @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix, + strbuf_realpath(&realpath, path->buf, 1); + + /* -+ * path and realpath are absolute; for comparison, we want -+ * startup_info->original_cwd to be an absolute path too. We -+ * can use strbuf_realpath for this. Also, if original_cwd -+ * started out as the empty string, then it corresponded to -+ * the top of the worktree, which is protected by other means -+ * so we just leave it blank. ++ * path and realpath are absolute; for comparison, we would ++ * like to transform startup_info->original_cwd to an absolute ++ * path too. + */ -+ if (*startup_info->original_cwd) ++ if (startup_info->original_cwd) + strbuf_realpath(&real_ocwd, + startup_info->original_cwd, 1); + @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix, return ret; ## t/t2501-cwd-empty.sh ## -@@ t/t2501-cwd-empty.sh: test_expect_failure 'apply does not remove cwd incidentally' ' - test_path_is_dir subdir - ' +@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () { + } --test_expect_failure 'clean does not remove cwd incidentally' ' -+test_expect_success 'clean does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'clean does not remove cwd incidentally' ' +- test_incidental_untracked_dir_removal failure \ +- git -C .. clean -fd -e warnings . >warnings ++ test_incidental_untracked_dir_removal success \ ++ git -C .. clean -fd -e warnings . >warnings && ++ grep "Refusing to remove current working directory" warnings + ' + test_expect_success 'stash does not remove cwd incidentally' ' -: ----------- > 7: 39b1f3a225e rebase: do not attempt to remove startup_info->original_cwd 7: edec0894ca2 ! 8: 0110462a19c stash: do not attempt to remove startup_info->original_cwd @@ Metadata ## Commit message ## stash: do not attempt to remove startup_info->original_cwd + Since stash spawns a `clean` subprocess, make sure we run that from the + startup_info->original_cwd directory, so that the `clean` processs knows + to protect that directory. Also, since the `clean` command might no + longer run from the toplevel, pass the ':/' magic pathspec to ensure we + still clean from the toplevel. + Signed-off-by: Elijah Newren <[email protected]> ## builtin/stash.c ## @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char cp.git_cmd = 1; + if (startup_info->original_cwd && -+ *startup_info->original_cwd && + !is_absolute_path(startup_info->original_cwd)) + cp.dir = startup_info->original_cwd; strvec_pushl(&cp.args, "clean", "--force", @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char ## t/t2501-cwd-empty.sh ## @@ t/t2501-cwd-empty.sh: test_expect_success 'clean does not remove cwd incidentally' ' - test_path_is_dir untracked ' --test_expect_failure 'stash does not remove cwd incidentally' ' -+test_expect_success 'stash does not remove cwd incidentally' ' - git checkout foo/bar/baz && - test_when_finished "git clean -fdx" && + test_expect_success 'stash does not remove cwd incidentally' ' +- test_incidental_untracked_dir_removal failure \ ++ test_incidental_untracked_dir_removal success \ + git stash --include-untracked + ' 8: 1815f18592b ! 9: 2c73a09a2e8 dir: avoid incidentally removing the original_cwd in remove_path() @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl); ## t/t2501-cwd-empty.sh ## @@ t/t2501-cwd-empty.sh: test_expect_success 'revert fails if cwd needs to be removed' ' - test_path_is_dir dirORfile ' --test_expect_failure 'rm does not remove cwd incidentally' ' -+test_expect_success 'rm does not remove cwd incidentally' ' - test_when_finished "git reset --hard" && - git checkout foo/bar/baz && + test_expect_success 'rm does not clean cwd incidentally' ' +- test_incidental_dir_removal failure git rm bar/baz.t ++ test_incidental_dir_removal success git rm bar/baz.t + ' -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rm does not remove cwd incidentally' ' - test_path_is_dir foo + test_expect_success 'apply does not remove cwd incidentally' ' + git diff HEAD HEAD~1 >patch && +- test_incidental_dir_removal failure git apply ../patch ++ test_incidental_dir_removal success git apply ../patch ' --test_expect_failure 'apply does not remove cwd incidentally' ' -+test_expect_success 'apply does not remove cwd incidentally' ' - test_when_finished "git reset --hard" && - git checkout foo/bar/baz && + test_incidental_untracked_dir_removal () { +@@ t/t2501-cwd-empty.sh: test_expect_success '`rm -rf dir` even with only tracked files will remove somet + ) && + + test_path_is_missing a/b/c/tracked && +- ## We would prefer if a/b was still present, though empty, since it +- ## was the current working directory +- #test_path_is_dir a/b +- ## But the current behavior is that it not only deletes the directory +- ## a/b as requested, but also goes and deletes a +- test_path_is_missing a ++ test_path_is_missing a/b/c && ++ test_path_is_dir a/b + ' + test_expect_success 'git version continues working from a deleted dir' ' 9: adaad7aeaac ! 10: d4e50b4053d dir: new flag to remove_dir_recurse() to spare the original_cwd @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl); * of the above REMOVE_DIR_* constants. Return 0 on success. ## t/t2501-cwd-empty.sh ## -@@ t/t2501-cwd-empty.sh: test_expect_success 'stash does not remove cwd incidentally' ' - test_path_is_dir untracked - ' +@@ t/t2501-cwd-empty.sh: test_submodule_removal () { + test_status= + test $path_status = dir && test_status=test_must_fail + +- # Actually, while path_status == dir && test_status=test_must_fail +- # reflect our desired behavior, current behavior is: +- path_status=missing +- test_status= +- + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf .git/modules/my_submodule" && -+test_expect_success 'rm -r leaves submodule if cwd inside' ' -+ test_when_finished "git reset --hard HEAD~1" && -+ test_when_finished "rm -rf .git/modules/my_submodule" && -+ -+ git checkout foo/bar/baz && -+ -+ git init my_submodule && -+ touch my_submodule/file && -+ git -C my_submodule add file && -+ git -C my_submodule commit -m "initial commit" && -+ git submodule add ./my_submodule && -+ git commit -m "Add the submodule" && -+ -+ ( -+ cd my_submodule && -+ test_must_fail git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/ -+ ) && -+ -+ test_path_is_dir my_submodule -+' -+ -+test_expect_success 'rm -rf removes submodule even if cwd inside' ' -+ test_when_finished "git reset --hard HEAD~1" && -+ test_when_finished "rm -rf .git/modules/my_submodule" && -+ -+ git checkout foo/bar/baz && -+ -+ git init my_submodule && -+ touch my_submodule/file && -+ git -C my_submodule add file && -+ git -C my_submodule commit -m "initial commit" && -+ git submodule add ./my_submodule && -+ git commit -m "Add the submodule" && -+ -+ ( -+ cd my_submodule && -+ git --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/ -+ ) && -+ -+ test_path_is_missing my_submodule -+' -+ - test_done -: ----------- > 11: 7eb6281be4b t2501: simplify the tests since we can now assume desired behavior
Hi Phillip,
Quoted 28 lines · Phillip Wood
Hi Elijah
Quoted 14 lines · Elijah Newren via GitGitGadget
Traditionally, if folks run git commands such as checkout or rebase from a subdirectory, that git command could remove their current working directory and result in subsequent git and non-git commands either getting confused or printing messages that confuse the user (e.g. "fatal: Unable to read current working directory: No such file or directory"). Many commands either silently avoid removing directories that are not empty (i.e. those that have untracked or modified files in them)[1], or show an error and abort, depending on which is more appropriate for the command in question. With this series, we augment the reasons to avoid removing directories to include not just has-untracked-or-modified-files, but also to avoid removing the
original_cwdas well.Peff and Junio provided some good pros/cons, if it helps:
Quoted 8 lines
* Pros: Peff (original suggester of the idea)[2], and Junio[3]
* Cons: Peff [2, again -- see the "P.S."], and Junio[4][1] well, with a few exceptions; see
https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/ [4]
https://lore.kernel.org/git/[email protected]/
Quoted 4 lines
Thanks for working on this, I'm sorry I haven't had time to take a proper look at it but I think it is a good idea. I did notice that you're using ":/" in patch 8 and wondered what happens if some runs 'git --literal-pathspecs stash' but I haven't looked properly.
Interesting point. The --literal-pathspecs passed to stash is not passed along to the git clean subprocess, and we only use the ":/" pathspec with this subprocess. This means whereas before we'd always run
git clean --force --quiet -d [-x]
from the toplevel, now we run
git clean --force --quiet -d :/ [-x]from the relevant subdirectory. So, I don't think I've introduced any bug here.
(Now, the fact that we use a git clean subprocess instead of just calling the appropriate library functions is IMO a design/implementation bug, but a longstanding one.)