* [PATCH v3 01/11] t2501: add various tests for removing the current working directory
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-27 10:32 ` Ævar Arnfjörð Bjarmason
2021-11-26 22:40 ` [PATCH v3 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
` (12 subsequent siblings)
13 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 333 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 333 insertions(+)
create mode 100755 t/t2501-cwd-empty.sh
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
new file mode 100755
index 00000000000..67feb6fd200
--- /dev/null
+++ b/t/t2501-cwd-empty.sh
@@ -0,0 +1,333 @@
+#!/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 [[ $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 &&
+
+ # 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" &&
+
+ (
+ cd dirORfile &&
+
+ # 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
+ ) &&
+
+ # 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_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 [[ $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 &&
+
+ # 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_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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v3 01/11] t2501: add various tests for removing the current working directory
2021-11-26 22:40 ` [PATCH v3 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-11-27 10:32 ` Ævar Arnfjörð Bjarmason
2021-11-27 19:16 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-27 10:32 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: git, Jeff King, René Scharfe, Glen Choo, Philip Oakley,
Elijah Newren
On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
> + # Although we want pwd & git status to pass, test for existing
> + # rather than desired behavior.
> + if [[ $works == "success" ]]; then
Wasn't "[[" bash-specific or something? In any case a more regular "if
test "$works" = "success" would work here.
> + pwd -P &&
I wonder if this is doing what we want everywhere cf. 482e1488a9b
(t0001: fix broken not-quite getcwd(3) test in bed67874e2, 2021-07-30),
but haven't looked much/thought about it.
> +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 testing the current behavior (and below) looks much better, thanks!
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 01/11] t2501: add various tests for removing the current working directory
2021-11-27 10:32 ` Ævar Arnfjörð Bjarmason
@ 2021-11-27 19:16 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-11-27 19:16 UTC (permalink / raw)
To: Ævar Arnfjörð Bjarmason
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Glen Choo, Philip Oakley
On Sat, Nov 27, 2021 at 2:35 AM Ævar Arnfjörð Bjarmason
<avarab@gmail.com> wrote:
>
> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
>
> > From: Elijah Newren <newren@gmail.com>
>
> > + # Although we want pwd & git status to pass, test for existing
> > + # rather than desired behavior.
> > + if [[ $works == "success" ]]; then
>
> Wasn't "[[" bash-specific or something? In any case a more regular "if
> test "$works" = "success" would work here.
Thanks; will fix.
> > + pwd -P &&
>
> I wonder if this is doing what we want everywhere cf. 482e1488a9b
> (t0001: fix broken not-quite getcwd(3) test in bed67874e2, 2021-07-30),
> but haven't looked much/thought about it.
I can change to `test-tool getcwd` for the obscure platforms; thanks
for the tip.
> > +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 testing the current behavior (and below) looks much better, thanks!
:-)
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-27 10:35 ` Ævar Arnfjörð Bjarmason
2021-11-27 10:40 ` Ævar Arnfjörð Bjarmason
2021-11-26 22:40 ` [PATCH v3 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (11 subsequent siblings)
13 siblings, 2 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 directory
Non-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
hello
This 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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
cache.h | 2 ++
common-main.c | 4 ++++
setup.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 71 insertions(+)
diff --git a/cache.h b/cache.h
index eba12487b99..92e181ea759 100644
--- a/cache.h
+++ b/cache.h
@@ -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;
diff --git a/common-main.c b/common-main.c
index 71e21dd20a3..aa8d5aba5bb 100644
--- a/common-main.c
+++ b/common-main.c
@@ -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);
diff --git a/setup.c b/setup.c
index 347d7181ae9..44f5bd38f7b 100644
--- a/setup.c
+++ b/setup.c
@@ -12,6 +12,7 @@ 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
@@ -432,6 +433,69 @@ void setup_work_tree(void)
initialized = 1;
}
+static void setup_original_cwd(void)
+{
+ struct strbuf tmp = STRBUF_INIT;
+ const char *worktree = NULL;
+ int offset = -1;
+
+ 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 removing.
+ *
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
+ *
+ * Yes, startup_info->original_cwd is usually the same as 'prefix',
+ * but differs in two ways:
+ * - prefix has a trailing '/'
+ * - if the user passes '-C' to git, that modifies the prefix but
+ * not startup_info->original_cwd.
+ */
+
+ /* Normalize the directory */
+ strbuf_realpath(&tmp, tmp_original_cwd, 1);
+ free((char*)tmp_original_cwd);
+ tmp_original_cwd = NULL;
+ startup_info->original_cwd = strbuf_detach(&tmp, NULL);
+
+ /*
+ * Get our worktree; we only protect the current working directory
+ * if it's in the worktree.
+ */
+ worktree = get_git_work_tree();
+ 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
+ */
+ startup_info->original_cwd = \
+ 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)
{
struct repository_format *data = vdata;
@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
}
+ setup_original_cwd();
strbuf_release(&dir);
strbuf_release(&gitdir);
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-27 10:35 ` Ævar Arnfjörð Bjarmason
2021-11-27 17:05 ` Elijah Newren
2021-11-27 10:40 ` Ævar Arnfjörð Bjarmason
1 sibling, 1 reply; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-27 10:35 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: git, Jeff King, René Scharfe, Glen Choo, Philip Oakley,
Elijah Newren
On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
> [...]
> + /*
> + * Get our worktree; we only protect the current working directory
> + * if it's in the worktree.
> + */
> + worktree = get_git_work_tree();
> + if (!worktree)
> + goto no_prevention_needed;
> +
> + offset = dir_inside_of(startup_info->original_cwd, worktree);
> + if (offset >= 0) {
Nit: Easier to read as:
offset = [...]
if (offset < 0)
return;
I.e. the reader can skip that whole "offset >= 0" block and anything
after if it <0, which also reduces the indentation. We run into the
"return' below.
> + /*
> + * 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
> + */
> + startup_info->original_cwd = \
TIL you can use backslashes like that in C outside of macros, but it's
not needed here, better without?
> + precompose_string_if_needed(startup_info->original_cwd
> + + offset);
> + }
> + return;
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-27 10:35 ` Ævar Arnfjörð Bjarmason
@ 2021-11-27 17:05 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-11-27 17:05 UTC (permalink / raw)
To: Ævar Arnfjörð Bjarmason
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Glen Choo, Philip Oakley
On Sat, Nov 27, 2021 at 2:39 AM Ævar Arnfjörð Bjarmason
<avarab@gmail.com> wrote:
>
>
> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
>
> > From: Elijah Newren <newren@gmail.com>
> > [...]
> > + /*
> > + * Get our worktree; we only protect the current working directory
> > + * if it's in the worktree.
> > + */
> > + worktree = get_git_work_tree();
> > + if (!worktree)
> > + goto no_prevention_needed;
> > +
> > + offset = dir_inside_of(startup_info->original_cwd, worktree);
> > + if (offset >= 0) {
>
> Nit: Easier to read as:
>
> offset = [...]
> if (offset < 0)
> return;
>
> I.e. the reader can skip that whole "offset >= 0" block and anything
> after if it <0, which also reduces the indentation. We run into the
> "return' below.
Whoops; the return should be inside the braces. I'll fix.
> > + /*
> > + * 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
> > + */
> > + startup_info->original_cwd = \
>
> TIL you can use backslashes like that in C outside of macros, but it's
> not needed here, better without?
Line is too long without it, so better with it.
>
> > + precompose_string_if_needed(startup_info->original_cwd
> > + + offset);
> > + }
> > + return;
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
2021-11-27 10:35 ` Ævar Arnfjörð Bjarmason
@ 2021-11-27 10:40 ` Ævar Arnfjörð Bjarmason
2021-11-27 18:31 ` Elijah Newren
1 sibling, 1 reply; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-27 10:40 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: git, Jeff King, René Scharfe, Glen Choo, Philip Oakley,
Elijah Newren
On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
[Just some more "I haven't really looked at this in all that much
detail" commentary, so maybe it's stupid, sorry]
> From: Elijah Newren <newren@gmail.com>
>
> 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 directory
>
> Non-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
> hello
Is that really realistic? Any "normal" command would use "pwd" or look
at $PWD, both of which "work", this error is only because we're starting
a new shell.
I wonder if it was just because you ran into our bin-wrappers edge case,
but that should be really obscure for any real users.
> This 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.
I'm still not clear at all on why we need a "original_cwd" at all then
as opposed to just using "prefix" (or adding "the_prefix" if passing it
down is painful). I.e. we discover our relative path, we resolve the
relative path to the root, can't we use that as the "don't remove our
CWD" guard?
Does our prefix change at some point, then maybe "orig_prefix" would
make more sense?
This is with the context that I haven't dug into your code, so maybe
there's some obvious reason I'm missing...
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-27 10:40 ` Ævar Arnfjörð Bjarmason
@ 2021-11-27 18:31 ` Elijah Newren
2021-11-28 18:04 ` Ævar Arnfjörð Bjarmason
0 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren @ 2021-11-27 18:31 UTC (permalink / raw)
To: Ævar Arnfjörð Bjarmason
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Glen Choo, Philip Oakley
On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason
<avarab@gmail.com> wrote:
>
> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
>
> [Just some more "I haven't really looked at this in all that much
> detail" commentary, so maybe it's stupid, sorry]
>
> > From: Elijah Newren <newren@gmail.com>
> >
> > 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 directory
> >
> > Non-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
> > hello
>
> Is that really realistic?
I have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my
Fedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu
workstation at $DAYJOB. That's not even considering stuff in other
directories. Note that I didn't place any of those scripts in
/usr/bin/; they came from the distribution and/or corporate packages
from others. Any invocation of bash will see the above "error"
message. Granted, bash calls it an 'error' but continues anyway, but
I thought it showed that there were clearly programs besides git where
users could run into problems.
> Any "normal" command would use "pwd" or look
> at $PWD, both of which "work", this error is only because we're starting
> a new shell.
<grin>
Yeah, good point. Who would use an unusual program like git? Or a
shell script? Or a java program? Or emacs? Or other programs like
them? Besides, git, bash, java, and emacs are all relatively young
commands with small development teams. Maybe we should just rely on
users only using commands that get the pwd/$PWD detail right; perhaps
limiting to commands that are more mature and have bigger development
teams than those four.
Silly users.
;-)
Sorry, couldn't resist a little friendly teasing.
However, for "pwd"...by "work", do you mean "doesn't necessarily
work"? On my machine:
$ mkdir gone
$ cd gone
$ rmdir ../gone
$ pwd -P
pwd: error retrieving current directory: getcwd: cannot access parent
directories: No such file or directory
$ pwd
pwd: error retrieving current directory: getcwd: cannot access parent
directories: No such file or directory
$ echo $PWD
/home/newren/floss/git/gone
If I do not run `pwd -P` first then a plain `pwd` works. But your
advice to use `pwd` seems misguided for programs, since they'll hit
this problem if users have run a previous `pwd -P`. (The $PWD trick
would have worked, as shown above)
> I wonder if it was just because you ran into our bin-wrappers edge case,
> but that should be really obscure for any real users.
Yes, I agree our bin-wrappers is a really obscure edge case not worth
considering. I don't see how you jump from there to painting all
shell scripts combined with the same brush, though.
> > This 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.
>
> I'm still not clear at all on why we need a "original_cwd" at all then
> as opposed to just using "prefix" (or adding "the_prefix" if passing it
> down is painful). I.e. we discover our relative path, we resolve the
> relative path to the root, can't we use that as the "don't remove our
> CWD" guard?
>
> Does our prefix change at some point, then maybe "orig_prefix" would
> make more sense?
No; see the code comment in the same patch:
+ /*
+ * startup_info->original_cwd points to the current working
+ * directory we inherited from our parent process, which is a
+ * directory we want to avoid removing.
+ *
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
+ *
+ * Yes, startup_info->original_cwd is usually the same as 'prefix',
+ * but differs in two ways:
+ * - prefix has a trailing '/'
+ * - if the user passes '-C' to git, that modifies the prefix but
+ * not startup_info->original_cwd.
+ */
It's never equal to prefix, even though it's usually semantically
referring to the same directory. However, even if it weren't for the
trailing slash issue, the -C case means it is not appropriate to think
of it as "orig_prefix" either.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-27 18:31 ` Elijah Newren
@ 2021-11-28 18:04 ` Ævar Arnfjörð Bjarmason
2021-11-29 21:58 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-28 18:04 UTC (permalink / raw)
To: Elijah Newren
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Glen Choo, Philip Oakley
On Sat, Nov 27 2021, Elijah Newren wrote:
> On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason
> <avarab@gmail.com> wrote:
>>
>> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
>>
>> [Just some more "I haven't really looked at this in all that much
>> detail" commentary, so maybe it's stupid, sorry]
>>
>> > From: Elijah Newren <newren@gmail.com>
>> >
>> > 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 directory
>> >
>> > Non-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
>> > hello
>>
>> Is that really realistic?
>
> I have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my
> Fedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu
> workstation at $DAYJOB. That's not even considering stuff in other
> directories. Note that I didn't place any of those scripts in
> /usr/bin/; they came from the distribution and/or corporate packages
> from others. Any invocation of bash will see the above "error"
> message. Granted, bash calls it an 'error' but continues anyway, but
> I thought it showed that there were clearly programs besides git where
> users could run into problems.
>
>> Any "normal" command would use "pwd" or look
>> at $PWD, both of which "work", this error is only because we're starting
>> a new shell.
>
> <grin>
>
> Yeah, good point. Who would use an unusual program like git? Or a
> shell script? Or a java program? Or emacs? Or other programs like
> them? Besides, git, bash, java, and emacs are all relatively young
> commands with small development teams. Maybe we should just rely on
> users only using commands that get the pwd/$PWD detail right; perhaps
> limiting to commands that are more mature and have bigger development
> teams than those four.
>
> Silly users.
>
> ;-)
>
> Sorry, couldn't resist a little friendly teasing.
>
>
> However, for "pwd"...by "work", do you mean "doesn't necessarily
> work"? On my machine:
>
> $ mkdir gone
> $ cd gone
> $ rmdir ../gone
> $ pwd -P
> pwd: error retrieving current directory: getcwd: cannot access parent
> directories: No such file or directory
> $ pwd
> pwd: error retrieving current directory: getcwd: cannot access parent
> directories: No such file or directory
> $ echo $PWD
> /home/newren/floss/git/gone
>
> If I do not run `pwd -P` first then a plain `pwd` works. But your
> advice to use `pwd` seems misguided for programs, since they'll hit
> this problem if users have run a previous `pwd -P`. (The $PWD trick
> would have worked, as shown above)
>
>> I wonder if it was just because you ran into our bin-wrappers edge case,
>> but that should be really obscure for any real users.
For some reason I was under the misimpression that the "#!/bin/bash"
part of the bin-wrappers and other scripts somehow immunized them from
the $PWD/"pwd" reset, and it was only the programs they invoked (like
git in the bin-wrappers) that didn't get the values passed along.
But that's clearly incorrect as you demonstrate above, so the only thing
that'll work OK (seemingly) is running "pwd" (but not "pwd -P"), or
looking at $PWD in your terminal shell itself.
Invoking non-POSIX shell programs "works" in that they can use the same
trick, after the dance of "mkdir x && cd x && rm -rf ..x" e.g. Perl
says:
$ perl -MCwd=getcwd -wE 'say $ENV{PWD}; say getcwd'
/home/avar/g/git/x
Use of uninitialized value in say at -e line 1
This "pwd -P" behavior isn't just something weird in your shell & mine,
it semse to be mandated by POSIX:
https://pubs.opengroup.org/onlinepubs/007904875/utilities/pwd.html
All of which is to say I'm much more sympathetic to this approach
now. I.e. it seemed like purely a way to work around fixable breakages
in our own and other programs. Well, I guess "don't use shellscript if
you care about this edge case" is a "fix", but not a trivial one.
I do still think a better starting point would be fixing the setup.c
dying in our own code, and see where that leaves us, but up to you
etc. I'm not going to work on it any time soon.
> Yes, I agree our bin-wrappers is a really obscure edge case not worth
> considering. I don't see how you jump from there to painting all
> shell scripts combined with the same brush, though.
*nod*
>> > This 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.
>>
>> I'm still not clear at all on why we need a "original_cwd" at all then
>> as opposed to just using "prefix" (or adding "the_prefix" if passing it
>> down is painful). I.e. we discover our relative path, we resolve the
>> relative path to the root, can't we use that as the "don't remove our
>> CWD" guard?
>>
>> Does our prefix change at some point, then maybe "orig_prefix" would
>> make more sense?
>
> No; see the code comment in the same patch:
>
> + /*
> + * startup_info->original_cwd points to the current working
> + * directory we inherited from our parent process, which is a
> + * directory we want to avoid removing.
> + *
> + * For convience, we would like to have the path relative to the
> + * worktree instead of an absolute path.
> + *
> + * Yes, startup_info->original_cwd is usually the same as 'prefix',
> + * but differs in two ways:
> + * - prefix has a trailing '/'
> + * - if the user passes '-C' to git, that modifies the prefix but
> + * not startup_info->original_cwd.
> + */
>
> It's never equal to prefix, even though it's usually semantically
> referring to the same directory. However, even if it weren't for the
> trailing slash issue, the -C case means it is not appropriate to think
> of it as "orig_prefix" either.
Ah, with -C of e.g. t/helper we'll first chdir(t/helper), and then run
the usual setup.c dance to find that we need to chdir() again to the
(equivalent of) "../../". But our prefix stays at "t/helper".
I'm a bit confused about the trailing slash case, isn't the prefix
always going to point to a directory? Why would t/helper v.s. t/helper/
matter?
I think it won't matter for rmdir(2) et al, but maybe I'm wrong.
What got me confused about the "prefix" v.s. "original_cwd" is that I
was assuming they'd be the same. The commentary on setup_git_directory()
says as much, i.e.:
Returns the "prefix", a path to the current working directory
relative to the work tree root, or NULL,
But of course we know that's a white lie, it's not the $PWD/getcwd(). So
you're only trying to save the user in cases of e.g. (in t/helper):
git rm ../helper
If they actually run it while in t/helper, but would like to explicitly
omit the case of (at the top-level):
git -C t/helper rm ../helper
That's fair enough I guess. I'd just assumed those cases would be
treated the same way.
Even in that case, I can't think of a case where this "original_cwd"
wouldn't be made redundant by some boolean flag to accompany "prefix" to
indicate that we've chdir'd out of the prefix (or not).
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd
2021-11-28 18:04 ` Ævar Arnfjörð Bjarmason
@ 2021-11-29 21:58 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-11-29 21:58 UTC (permalink / raw)
To: Ævar Arnfjörð Bjarmason
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Glen Choo, Philip Oakley
On Mon, Nov 29, 2021 at 12:41 PM Ævar Arnfjörð Bjarmason
<avarab@gmail.com> wrote:
>
>
> On Sat, Nov 27 2021, Elijah Newren wrote:
>
> > On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason
> > <avarab@gmail.com> wrote:
> >>
> >> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:
> >>
> >> [Just some more "I haven't really looked at this in all that much
> >> detail" commentary, so maybe it's stupid, sorry]
> >>
> >> > From: Elijah Newren <newren@gmail.com>
> >> >
> >> > 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 directory
> >> >
> >> > Non-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
> >> > hello
> >>
> >> Is that really realistic?
> >
> > I have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my
> > Fedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu
> > workstation at $DAYJOB. That's not even considering stuff in other
> > directories. Note that I didn't place any of those scripts in
> > /usr/bin/; they came from the distribution and/or corporate packages
> > from others. Any invocation of bash will see the above "error"
> > message. Granted, bash calls it an 'error' but continues anyway, but
> > I thought it showed that there were clearly programs besides git where
> > users could run into problems.
> >
> >> Any "normal" command would use "pwd" or look
> >> at $PWD, both of which "work", this error is only because we're starting
> >> a new shell.
> >
> > <grin>
> >
> > Yeah, good point. Who would use an unusual program like git? Or a
> > shell script? Or a java program? Or emacs? Or other programs like
> > them? Besides, git, bash, java, and emacs are all relatively young
> > commands with small development teams. Maybe we should just rely on
> > users only using commands that get the pwd/$PWD detail right; perhaps
> > limiting to commands that are more mature and have bigger development
> > teams than those four.
> >
> > Silly users.
> >
> > ;-)
> >
> > Sorry, couldn't resist a little friendly teasing.
> >
> >
> > However, for "pwd"...by "work", do you mean "doesn't necessarily
> > work"? On my machine:
> >
> > $ mkdir gone
> > $ cd gone
> > $ rmdir ../gone
> > $ pwd -P
> > pwd: error retrieving current directory: getcwd: cannot access parent
> > directories: No such file or directory
> > $ pwd
> > pwd: error retrieving current directory: getcwd: cannot access parent
> > directories: No such file or directory
> > $ echo $PWD
> > /home/newren/floss/git/gone
> >
> > If I do not run `pwd -P` first then a plain `pwd` works. But your
> > advice to use `pwd` seems misguided for programs, since they'll hit
> > this problem if users have run a previous `pwd -P`. (The $PWD trick
> > would have worked, as shown above)
> >
> >> I wonder if it was just because you ran into our bin-wrappers edge case,
> >> but that should be really obscure for any real users.
>
> For some reason I was under the misimpression that the "#!/bin/bash"
> part of the bin-wrappers and other scripts somehow immunized them from
> the $PWD/"pwd" reset, and it was only the programs they invoked (like
> git in the bin-wrappers) that didn't get the values passed along.
>
> But that's clearly incorrect as you demonstrate above, so the only thing
> that'll work OK (seemingly) is running "pwd" (but not "pwd -P"), or
> looking at $PWD in your terminal shell itself.
And further, even running a plain "pwd" seems to not work either if
something else has already run "pwd -P" already, as I pointed out
above. I have no clue how one 'pwd -P' call can prevent future plain
'pwd' calls from working, but that's the behavior I observed.
> Invoking non-POSIX shell programs "works" in that they can use the same
> trick, after the dance of "mkdir x && cd x && rm -rf ..x" e.g. Perl
> says:
>
> $ perl -MCwd=getcwd -wE 'say $ENV{PWD}; say getcwd'
> /home/avar/g/git/x
> Use of uninitialized value in say at -e line 1
>
> This "pwd -P" behavior isn't just something weird in your shell & mine,
> it semse to be mandated by POSIX:
> https://pubs.opengroup.org/onlinepubs/007904875/utilities/pwd.html
>
> All of which is to say I'm much more sympathetic to this approach
> now. I.e. it seemed like purely a way to work around fixable breakages
> in our own and other programs. Well, I guess "don't use shellscript if
> you care about this edge case" is a "fix", but not a trivial one.
>
> I do still think a better starting point would be fixing the setup.c
> dying in our own code, and see where that leaves us, but up to you
> etc. I'm not going to work on it any time soon.
>
> > Yes, I agree our bin-wrappers is a really obscure edge case not worth
> > considering. I don't see how you jump from there to painting all
> > shell scripts combined with the same brush, though.
>
> *nod*
>
> >> > This 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.
> >>
> >> I'm still not clear at all on why we need a "original_cwd" at all then
> >> as opposed to just using "prefix" (or adding "the_prefix" if passing it
> >> down is painful). I.e. we discover our relative path, we resolve the
> >> relative path to the root, can't we use that as the "don't remove our
> >> CWD" guard?
> >>
> >> Does our prefix change at some point, then maybe "orig_prefix" would
> >> make more sense?
> >
> > No; see the code comment in the same patch:
> >
> > + /*
> > + * startup_info->original_cwd points to the current working
> > + * directory we inherited from our parent process, which is a
> > + * directory we want to avoid removing.
> > + *
> > + * For convience, we would like to have the path relative to the
> > + * worktree instead of an absolute path.
> > + *
> > + * Yes, startup_info->original_cwd is usually the same as 'prefix',
> > + * but differs in two ways:
> > + * - prefix has a trailing '/'
> > + * - if the user passes '-C' to git, that modifies the prefix but
> > + * not startup_info->original_cwd.
> > + */
> >
> > It's never equal to prefix, even though it's usually semantically
> > referring to the same directory. However, even if it weren't for the
> > trailing slash issue, the -C case means it is not appropriate to think
> > of it as "orig_prefix" either.
>
> Ah, with -C of e.g. t/helper we'll first chdir(t/helper), and then run
> the usual setup.c dance to find that we need to chdir() again to the
> (equivalent of) "../../". But our prefix stays at "t/helper".
>
> I'm a bit confused about the trailing slash case, isn't the prefix
> always going to point to a directory? Why would t/helper v.s. t/helper/
> matter?
Because when you compare two paths, you either need to canonicalize
them in some fashion or do a bunch of tricky work to see if they are
"equal".
> I think it won't matter for rmdir(2) et al, but maybe I'm wrong.
Correct, rmdir won't care, but strcmp() certainly does. Since we call
strcmp() before deciding whether to remove, and we do that based on
paths built up from prefix + tree traversal + entry name (which won't
have the trailing '/') we want original_cmp to be canonicalized the
same way (i.e. without the trailing '/').
(In contrast, prefix is used as a starting point to build up other
paths, so having a trailing slash was more convenient for it.)
> What got me confused about the "prefix" v.s. "original_cwd" is that I
> was assuming they'd be the same. The commentary on setup_git_directory()
> says as much, i.e.:
>
> Returns the "prefix", a path to the current working directory
> relative to the work tree root, or NULL,
>
> But of course we know that's a white lie, it's not the $PWD/getcwd().
I don't think it is a white lie. This code runs after the chdir()
from the parsing of the global -C option, and before the chdir() to
the worktree root (which occurs later in setup.c). So the statement
is true, you just have to understand what "current" means in "the
current working directory". In this context, "current" means *as of
the time the setup.c code where this comment is written starts
running*, which is a pretty logical thing for current to mean. If you
try reading this code unaware of the other chdir() calls we make,
though, it certainly has the potential to trip you up.
> So you're only trying to save the user in cases of e.g. (in t/helper):
>
> git rm ../helper
>
> If they actually run it while in t/helper, but would like to explicitly
> omit the case of (at the top-level):
>
> git -C t/helper rm ../helper
>
> That's fair enough I guess. I'd just assumed those cases would be
> treated the same way.
I don't see why you'd assume that. The whole point was preserving the
cwd of our parent process so that any subsequent commands that happen
to run in that parent process don't get confused by having their cwd
no longer exist. In the former command you gave, the cwd of the
parent process was $WORKTREE/t/helper; in the latter, it was
$WORKTREE. So, they should preserve different things.
> Even in that case, I can't think of a case where this "original_cwd"
> wouldn't be made redundant by some boolean flag to accompany "prefix" to
> indicate that we've chdir'd out of the prefix (or not).
I can't think of a case where a boolean would provide any useful
information. Don't we always chdir out of the prefix whenever prefix
!= NULL?
Maybe I'm missing something, so let me give you a simple case where
you can explain how this boolean could help. Let's say you started at
the worktree root and then ran these two commands:
$ cd subdir/
$ git -C subsubdir rm lastfile
After finishing the setup phase of the git command, the state should
be the following:
prefix = subdir/subsubdir/
original_cwd = subdir
getcwd = subdir/../ (because setup.c chdirs to the worktree root;
the actual answer would be a more canonicalized path, though)
In this above case, how can we infer the value of original_cwd from
other data if we don't record it?
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v3 03/11] unpack-trees: refuse to remove startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
` (10 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 20 +++++++-------------
unpack-trees.c | 17 +++++++++++++----
unpack-trees.h | 1 +
3 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 67feb6fd200..52399d1906f 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -106,7 +106,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' '
@@ -137,23 +137,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' '
@@ -161,7 +155,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' '
@@ -177,7 +171,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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 89ca95ce90b..6bc16f3a714 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
}
diff --git a/unpack-trees.h b/unpack-trees.h
index 71ffb7eeb0c..efb9edfbb27 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -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,
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 04/11] unpack-trees: add special cwd handling
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (2 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
` (9 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 2 +-
unpack-trees.c | 13 +++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 52399d1906f..886a391a63d 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -114,7 +114,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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 6bc16f3a714..5852807d2fb 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 05/11] symlinks: do not include startup_info->original_cwd in dir removal
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (3 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (8 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
symlinks.c | 8 +++++++-
t/t2501-cwd-empty.sh | 10 +++++-----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/symlinks.c b/symlinks.c
index 5232d02020c..c667baa949b 100644
--- a/symlinks.c
+++ b/symlinks.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 886a391a63d..496f4c6a6e9 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -102,7 +102,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' '
@@ -110,7 +110,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' '
@@ -118,7 +118,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
@@ -151,7 +151,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' '
@@ -167,7 +167,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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 06/11] clean: do not attempt to remove startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (4 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-26 22:40 ` [PATCH v3 07/11] rebase: " Elijah Newren via GitGitGadget
` (7 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/clean.c | 44 +++++++++++++++++++++++++++++++++++---------
t/t2501-cwd-empty.sh | 5 +++--
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/builtin/clean.c b/builtin/clean.c
index 98a2860409b..3ff02bbbffe 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -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;
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 496f4c6a6e9..be04b673f6a 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -221,8 +221,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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (5 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-29 17:50 ` Derrick Stolee
2021-11-26 22:40 ` [PATCH v3 08/11] stash: " Elijah Newren via GitGitGadget
` (6 subsequent siblings)
13 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
sequencer.c | 3 +++
t/t2501-cwd-empty.sh | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index ea96837cde3..b71f7b8a0a6 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,
cmd.git_cmd = 1;
+ if (startup_info->original_cwd &&
+ !is_absolute_path(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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index be04b673f6a..acf9646ec6e 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -159,11 +159,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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2021-11-29 17:50 ` Derrick Stolee
2021-11-29 19:22 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Derrick Stolee @ 2021-11-29 17:50 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget, git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley
On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
>
> 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.
>
> Signed-off-by: Elijah Newren <newren@gmail.com>
> ---
> sequencer.c | 3 +++
> t/t2501-cwd-empty.sh | 4 ++--
> 2 files changed, 5 insertions(+), 2 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index ea96837cde3..b71f7b8a0a6 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,
>
> cmd.git_cmd = 1;
>
> + if (startup_info->original_cwd &&
> + !is_absolute_path(startup_info->original_cwd))
> + cmd.dir = startup_info->original_cwd;
I was initially confused by the "!is_absolute_path()" because
it seemed to me like it would be natural to store an absolute
path there, but I see this comment in patch 2:
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
So it seems that we won't store it as an absolute path. Is
there any value in this condition, then?
This assignment of cmd.dir to the relative path has a lot
of baked-in knowledge of this variable _and_ the current
state (Git chdir()'d to the root of the worktree). If the
path is always relative, then it should be a BUG() if we
see an absolute path. Also, it seems like we would want
cmd.dir to be a concatenation of the worktree root and the
original_cwd.
Or perhaps I'm being overly cautious and this could all be
resolved with a comment about the expected state of the
working directory and original_cwd. The tests will catch if
any of those expectations change.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-11-29 17:50 ` Derrick Stolee
@ 2021-11-29 19:22 ` Elijah Newren
2021-11-29 19:42 ` Derrick Stolee
0 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren @ 2021-11-29 19:22 UTC (permalink / raw)
To: Derrick Stolee
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley
On Mon, Nov 29, 2021 at 9:50 AM Derrick Stolee <stolee@gmail.com> wrote:
>
> On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:
> > From: Elijah Newren <newren@gmail.com>
> >
> > 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.
> >
> > Signed-off-by: Elijah Newren <newren@gmail.com>
> > ---
> > sequencer.c | 3 +++
> > t/t2501-cwd-empty.sh | 4 ++--
> > 2 files changed, 5 insertions(+), 2 deletions(-)
> >
> > diff --git a/sequencer.c b/sequencer.c
> > index ea96837cde3..b71f7b8a0a6 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,
> >
> > cmd.git_cmd = 1;
> >
> > + if (startup_info->original_cwd &&
> > + !is_absolute_path(startup_info->original_cwd))
> > + cmd.dir = startup_info->original_cwd;
>
> I was initially confused by the "!is_absolute_path()" because
> it seemed to me like it would be natural to store an absolute
> path there, but I see this comment in patch 2:
>
> + * For convience, we would like to have the path relative to the
> + * worktree instead of an absolute path.
>
> So it seems that we won't store it as an absolute path. Is
> there any value in this condition, then?
Good catch. This is leftover from an earlier round; I'll remove it.
From stash too.
> This assignment of cmd.dir to the relative path has a lot
> of baked-in knowledge of this variable _and_ the current
> state (Git chdir()'d to the root of the worktree). If the
> path is always relative, then it should be a BUG() if we
> see an absolute path. Also, it seems like we would want
> cmd.dir to be a concatenation of the worktree root and the
> original_cwd.
>
> Or perhaps I'm being overly cautious and this could all be
> resolved with a comment about the expected state of the
> working directory and original_cwd. The tests will catch if
> any of those expectations change.
Yeah, with the extra condition in there I should have added a good
explanation about why I had it in there. But I think a simple
if (startup_info->original_cwd)
cmd.dir = startup_info->original_cwd;
should be good enough? Or do you still want a comment for that?
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-11-29 19:22 ` Elijah Newren
@ 2021-11-29 19:42 ` Derrick Stolee
0 siblings, 0 replies; 163+ messages in thread
From: Derrick Stolee @ 2021-11-29 19:42 UTC (permalink / raw)
To: Elijah Newren
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley
On 11/29/2021 2:22 PM, Elijah Newren wrote:
> On Mon, Nov 29, 2021 at 9:50 AM Derrick Stolee <stolee@gmail.com> wrote:
>>
>> On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:
>>> cmd.git_cmd = 1;
>>>
>>> + if (startup_info->original_cwd &&
>>> + !is_absolute_path(startup_info->original_cwd))
>>> + cmd.dir = startup_info->original_cwd;
>>
>> I was initially confused by the "!is_absolute_path()" because
>> it seemed to me like it would be natural to store an absolute
>> path there, but I see this comment in patch 2:
>>
>> + * For convience, we would like to have the path relative to the
>> + * worktree instead of an absolute path.
>>
>> So it seems that we won't store it as an absolute path. Is
>> there any value in this condition, then?
>
> Good catch. This is leftover from an earlier round; I'll remove it.
> From stash too.
>
>> This assignment of cmd.dir to the relative path has a lot
>> of baked-in knowledge of this variable _and_ the current
>> state (Git chdir()'d to the root of the worktree). If the
>> path is always relative, then it should be a BUG() if we
>> see an absolute path. Also, it seems like we would want
>> cmd.dir to be a concatenation of the worktree root and the
>> original_cwd.
>>
>> Or perhaps I'm being overly cautious and this could all be
>> resolved with a comment about the expected state of the
>> working directory and original_cwd. The tests will catch if
>> any of those expectations change.
>
> Yeah, with the extra condition in there I should have added a good
> explanation about why I had it in there. But I think a simple
>
> if (startup_info->original_cwd)
> cmd.dir = startup_info->original_cwd;
>
> should be good enough? Or do you still want a comment for that?
This is the code I would expect to see. No comment needed.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v3 08/11] stash: do not attempt to remove startup_info->original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (6 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2021-11-26 22:40 ` Elijah Newren via GitGitGadget
2021-11-26 22:41 ` [PATCH v3 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
` (5 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 <newren@gmail.com>
---
builtin/stash.c | 5 ++++-
t/t2501-cwd-empty.sh | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/builtin/stash.c b/builtin/stash.c
index a0ccc8654df..4d89cc7f969 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1485,8 +1485,11 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
struct child_process cp = CHILD_PROCESS_INIT;
cp.git_cmd = 1;
+ if (startup_info->original_cwd &&
+ !is_absolute_path(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)) {
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index acf9646ec6e..6788a0d267f 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -227,7 +227,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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 09/11] dir: avoid incidentally removing the original_cwd in remove_path()
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (7 preceding siblings ...)
2021-11-26 22:40 ` [PATCH v3 08/11] stash: " Elijah Newren via GitGitGadget
@ 2021-11-26 22:41 ` Elijah Newren via GitGitGadget
2021-11-26 22:41 ` [PATCH v3 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
` (4 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:41 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
dir.c | 3 +++
dir.h | 6 +++++-
t/t2501-cwd-empty.sh | 12 ++++--------
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/dir.c b/dir.c
index 94489298f4c..97d6b71c872 100644
--- a/dir.c
+++ b/dir.c
@@ -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);
}
diff --git a/dir.h b/dir.h
index 83f46c0fb4c..d6a5d03bec2 100644
--- a/dir.h
+++ b/dir.h
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 6788a0d267f..cba817ff734 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -175,12 +175,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 () {
@@ -262,12 +262,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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (8 preceding siblings ...)
2021-11-26 22:41 ` [PATCH v3 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
@ 2021-11-26 22:41 ` Elijah Newren via GitGitGadget
2021-11-26 22:41 ` [PATCH v3 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
` (3 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:41 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/rm.c | 3 ++-
dir.c | 12 +++++++++---
dir.h | 3 +++
t/t2501-cwd-empty.sh | 5 -----
4 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/builtin/rm.c b/builtin/rm.c
index 3d0967cdc11..b4132e5d8ee 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -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;
diff --git a/dir.c b/dir.c
index 97d6b71c872..52064345a6b 100644
--- a/dir.c
+++ b/dir.c
@@ -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.
diff --git a/dir.h b/dir.h
index d6a5d03bec2..8e02dfb505d 100644
--- a/dir.h
+++ b/dir.h
@@ -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.
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index cba817ff734..8f299fd3b19 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -282,11 +282,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" &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v3 11/11] t2501: simplify the tests since we can now assume desired behavior
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (9 preceding siblings ...)
2021-11-26 22:41 ` [PATCH v3 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
@ 2021-11-26 22:41 ` Elijah Newren via GitGitGadget
2021-11-29 17:57 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
` (2 subsequent siblings)
13 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-26 22:41 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
We no longer are dealing with a mixture of previous and desired
behavior, so simplify the tests a bit.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 105 +++++++++++--------------------------------
1 file changed, 27 insertions(+), 78 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 8f299fd3b19..88e27b16e11 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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,81 +41,49 @@ test_incidental_dir_removal () {
cd foo &&
"$@" &&
- # 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
+ pwd -P &&
+ 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 [[ $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 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_must_fail "$@" 2>../error &&
+ grep "Refusing to remove.*current working directory" ../error &&
+ pwd -P &&
+ git status --porcelain
) &&
- # 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_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
@@ -147,46 +112,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} &&
@@ -198,36 +160,23 @@ test_incidental_untracked_dir_removal () {
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
+ pwd -P &&
+ 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 [[ $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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (10 preceding siblings ...)
2021-11-26 22:41 ` [PATCH v3 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
@ 2021-11-29 17:57 ` Derrick Stolee
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
2021-11-30 11:04 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Phillip Wood
13 siblings, 0 replies; 163+ messages in thread
From: Derrick Stolee @ 2021-11-29 17:57 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget, git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley
On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:
> 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_cwd as well.
As I was reviewing v2, this version popped up in my inbox. Sorry about that.
The only actionable comment from my review of v2 was the addition of a check
that the worktree is in the expected state after commands are aborted due to
trying to remove the current working directory. Your suggested
git diff --exit-code HEAD
should work for this. I might even add a "git rev-parse HEAD" to make sure
we are on the right ref, but that's perhaps too specific to something like
'git reset --hard <branch>'.
> 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
Looking at these changes I like all of them.
> Range-diff vs v2:
>
> 1: 38a120f5c03 ! 1: 4b0044656b0 t2501: add various tests for removing the current working directory
I like this new test structure. Using test_expect_success to document existing
behavior is a good strategy.
> 2: f6129a8ac9c ! 2: 200ddece05d setup: introduce startup_info->original_cwd> 3: e74975e83cc ! 3: 68ae90546fe unpack-trees: refuse to remove startup_info->original_cwd
> 4: e06806e3a32 ! 4: 1bb8905900c unpack-trees: add special cwd handling
> 5: 46728f74ea1 ! 5: 8a33d74e7cf symlinks: do not include startup_info->original_cwd in dir removal
> 6: 01ce9444dae ! 6: 11e4ec881bb clean: do not attempt to remove startup_info->original_cwd
These changes looked good.
> -: ----------- > 7: 39b1f3a225e rebase: do not attempt to remove startup_info->original_cwd
I had a small comment on this one, only because I think there is a condition
in your 'if' statement that is either unhelpful or is hiding something.
> 7: edec0894ca2 ! 8: 0110462a19c stash: do not attempt to remove startup_info->original_cwd
...
> 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;
And here is a similar use of is_absolute_path() that could be resolved the
same way as whatever you choose for the v3 patch 7.
> 8: 1815f18592b ! 9: 2c73a09a2e8 dir: avoid incidentally removing the original_cwd in remove_path()
> 9: adaad7aeaac ! 10: d4e50b4053d dir: new flag to remove_dir_recurse() to spare the original_cwd
> -: ----------- > 11: 7eb6281be4b t2501: simplify the tests since we can now assume desired behavior
This last test cleanup is good to have.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (11 preceding siblings ...)
2021-11-29 17:57 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
` (12 more replies)
2021-11-30 11:04 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Phillip Wood
13 siblings, 13 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren
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_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/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/
[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/
[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]
https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/
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-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v4
Pull-Request: https://github.com/git/git/pull/1140
Range-diff vs v3:
1: 4b0044656b0 ! 1: a45b3f08802 t2501: add various tests for removing the current working directory
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want pwd & git status to pass, test for existing
+ # rather than desired behavior.
-+ if [[ $works == "success" ]]; then
-+ pwd -P &&
++ if test "$works" == "success"
++ then
++ test-tool getcwd &&
+ git status --porcelain
+ else
-+ ! pwd -P &&
++ ! test-tool getcwd &&
+ test_might_fail git status --porcelain
+ fi
+ ) &&
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want dir to be present, test for existing rather
+ # than desired behavior.
-+ if [[ $works == "success" ]]; then
++ if test "$works" == "success"
++ then
+ test_path_is_dir foo
+ else
+ test_path_is_missing foo
@@ t/t2501-cwd-empty.sh (new)
+ cd dirORfile &&
+
+ # 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
++ # 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 &&
-+ pwd -P &&
++
++ git diff --exit-code HEAD &&
++
++ test-tool getcwd &&
+ git status --porcelain
+ else
+ "$@" &&
-+ ! pwd -P &&
++ ! 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 [[ $works == "success" ]]; then
++ if test "$works" == "success"
++ then
+ test_path_is_dir dirORfile
+ else
+ test_path_is_file dirORfile
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want pwd & git status to pass, test for existing
+ # rather than desired behavior.
-+ if [[ $works == "success" ]]; then
-+ pwd -P &&
++ if test "$works" == "success"
++ then
++ test-tool getcwd &&
+ git status --porcelain
+ else
-+ ! pwd -P &&
++ ! test-tool getcwd &&
+ test_might_fail git status --porcelain
+ fi
+ ) &&
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want dir to be present, test for existing rather
+ # than desired behavior.
-+ if [[ $works == "success" ]]; then
++ if test "$works" == "success"
++ then
+ test_path_is_dir untracked
+ else
+ test_path_is_missing untracked
@@ t/t2501-cwd-empty.sh (new)
+ shift &&
+
+ test_status=
-+ test $path_status = dir && test_status=test_must_fail
++ 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:
2: 200ddece05d ! 2: ca9f632bd11 setup: introduce startup_info->original_cwd
@@ setup.c: void setup_work_tree(void)
+ startup_info->original_cwd = \
+ precompose_string_if_needed(startup_info->original_cwd
+ + offset);
++ return;
+ }
-+ return;
+
+no_prevention_needed:
+ free((char*)startup_info->original_cwd);
3: 68ae90546fe = 3: 41a82eff41e unpack-trees: refuse to remove startup_info->original_cwd
4: 1bb8905900c = 4: 2e2ea02f97b unpack-trees: add special cwd handling
5: 8a33d74e7cf = 5: f444a541da4 symlinks: do not include startup_info->original_cwd in dir removal
6: 11e4ec881bb = 6: 1990e36bb41 clean: do not attempt to remove startup_info->original_cwd
7: 39b1f3a225e ! 7: 1035ee7f9ce rebase: do not attempt to remove startup_info->original_cwd
@@ sequencer.c: static int run_git_checkout(struct repository *r, struct replay_opt
cmd.git_cmd = 1;
-+ if (startup_info->original_cwd &&
-+ !is_absolute_path(startup_info->original_cwd))
++ if (startup_info->original_cwd)
+ cmd.dir = startup_info->original_cwd;
strvec_push(&cmd.args, "checkout");
strvec_push(&cmd.args, commit);
8: 0110462a19c ! 8: a2be40a22d1 stash: do not attempt to remove startup_info->original_cwd
@@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char
struct child_process cp = CHILD_PROCESS_INIT;
cp.git_cmd = 1;
-+ if (startup_info->original_cwd &&
-+ !is_absolute_path(startup_info->original_cwd))
++ if (startup_info->original_cwd)
+ cp.dir = startup_info->original_cwd;
strvec_pushl(&cp.args, "clean", "--force",
- "--quiet", "-d", NULL);
9: 2c73a09a2e8 = 9: 834031be9e0 dir: avoid incidentally removing the original_cwd in remove_path()
10: d4e50b4053d ! 10: d5750fcb6d5 dir: new flag to remove_dir_recurse() to spare the original_cwd
@@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);
## t/t2501-cwd-empty.sh ##
@@ t/t2501-cwd-empty.sh: test_submodule_removal () {
test_status=
- test $path_status = dir && test_status=test_must_fail
+ 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:
11: 7eb6281be4b ! 11: 21ff99a767c t2501: simplify the tests since we can now assume desired behavior
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # Although we want pwd & git status to pass, test for existing
- # rather than desired behavior.
-- if [[ $works == "success" ]]; then
-- pwd -P &&
+- if test "$works" == "success"
+- then
+- test-tool getcwd &&
- git status --porcelain
- else
-- ! pwd -P &&
+- ! test-tool getcwd &&
- test_might_fail git status --porcelain
- fi
-+ pwd -P &&
++ # Make sure foo still exists, and commands needing it work
++ test-tool getcwd &&
+ git status --porcelain
) &&
test_path_is_missing foo/bar/baz &&
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # Although we want dir to be present, test for existing rather
- # than desired behavior.
-- if [[ $works == "success" ]]; then
+- if test "$works" == "success"
+- then
- test_path_is_dir foo
- else
- test_path_is_missing foo
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
cd dirORfile &&
- # 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
+- # 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 &&
-- pwd -P &&
+-
+- git diff --exit-code HEAD &&
+-
+- test-tool getcwd &&
- git status --porcelain
- else
- "$@" &&
-- ! pwd -P &&
+- ! 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 &&
-+ pwd -P &&
++
++ # ...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 [[ $works == "success" ]]; then
+- if test "$works" == "success"
+- then
- test_path_is_dir dirORfile
- else
- test_path_is_file dirORfile
@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {
- # Although we want pwd & git status to pass, test for existing
- # rather than desired behavior.
-- if [[ $works == "success" ]]; then
-- pwd -P &&
+- if test "$works" == "success"
+- then
+- test-tool getcwd &&
- git status --porcelain
- else
-- ! pwd -P &&
+- ! test-tool getcwd &&
- test_might_fail git status --porcelain
- fi
-+ pwd -P &&
++ # Make sure untracked still exists, and commands needing it work
++ test-tool getcwd &&
+ git status --porcelain
) &&
test_path_is_missing empty &&
@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {
- # Although we want dir to be present, test for existing rather
- # than desired behavior.
-- if [[ $works == "success" ]]; then
+- if test "$works" == "success"
+- then
- test_path_is_dir untracked
- else
- test_path_is_missing untracked
--
gitgitgadget
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v4 01/11] t2501: add various tests for removing the current working directory
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-30 6:47 ` Junio C Hamano
2021-11-29 22:37 ` [PATCH v4 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
` (11 subsequent siblings)
12 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 342 insertions(+)
create mode 100755 t/t2501-cwd-empty.sh
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
new file mode 100755
index 00000000000..fd83fe921d5
--- /dev/null
+++ b/t/t2501-cwd-empty.sh
@@ -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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v4 01/11] t2501: add various tests for removing the current working directory
2021-11-29 22:37 ` [PATCH v4 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-11-30 6:47 ` Junio C Hamano
2021-11-30 6:53 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Junio C Hamano @ 2021-11-30 6:47 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: git, Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee
"Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> + # Although we want pwd & git status to pass, test for existing
> + # rather than desired behavior.
> + if test "$works" == "success"
Don't double "=" here. We are not writing for bash. There are a
few more instances of the same mistake in this patch.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v4 01/11] t2501: add various tests for removing the current working directory
2021-11-30 6:47 ` Junio C Hamano
@ 2021-11-30 6:53 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-11-30 6:53 UTC (permalink / raw)
To: Junio C Hamano
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley, Derrick Stolee
On Mon, Nov 29, 2021 at 10:47 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > + # Although we want pwd & git status to pass, test for existing
> > + # rather than desired behavior.
> > + if test "$works" == "success"
>
> Don't double "=" here. We are not writing for bash. There are a
> few more instances of the same mistake in this patch.
Yep, also pointed out by Eric. I've got a v5 queued up whose change
is just fixing this, but I'm waiting a bit to see if other items are
spotted in review before sending.
Thanks for spotting.
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v4 02/11] setup: introduce startup_info->original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (10 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 directory
Non-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
hello
This 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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
cache.h | 2 ++
common-main.c | 4 ++++
setup.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 71 insertions(+)
diff --git a/cache.h b/cache.h
index eba12487b99..92e181ea759 100644
--- a/cache.h
+++ b/cache.h
@@ -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;
diff --git a/common-main.c b/common-main.c
index 71e21dd20a3..aa8d5aba5bb 100644
--- a/common-main.c
+++ b/common-main.c
@@ -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);
diff --git a/setup.c b/setup.c
index 347d7181ae9..af3b8c09abe 100644
--- a/setup.c
+++ b/setup.c
@@ -12,6 +12,7 @@ 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
@@ -432,6 +433,69 @@ void setup_work_tree(void)
initialized = 1;
}
+static void setup_original_cwd(void)
+{
+ struct strbuf tmp = STRBUF_INIT;
+ const char *worktree = NULL;
+ int offset = -1;
+
+ 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 removing.
+ *
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
+ *
+ * Yes, startup_info->original_cwd is usually the same as 'prefix',
+ * but differs in two ways:
+ * - prefix has a trailing '/'
+ * - if the user passes '-C' to git, that modifies the prefix but
+ * not startup_info->original_cwd.
+ */
+
+ /* Normalize the directory */
+ strbuf_realpath(&tmp, tmp_original_cwd, 1);
+ free((char*)tmp_original_cwd);
+ tmp_original_cwd = NULL;
+ startup_info->original_cwd = strbuf_detach(&tmp, NULL);
+
+ /*
+ * Get our worktree; we only protect the current working directory
+ * if it's in the worktree.
+ */
+ worktree = get_git_work_tree();
+ 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
+ */
+ startup_info->original_cwd = \
+ 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)
{
struct repository_format *data = vdata;
@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
}
+ setup_original_cwd();
strbuf_release(&dir);
strbuf_release(&gitdir);
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 03/11] unpack-trees: refuse to remove startup_info->original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
` (9 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 20 +++++++-------------
unpack-trees.c | 17 +++++++++++++----
unpack-trees.h | 1 +
3 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index fd83fe921d5..55081f5c980 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 89ca95ce90b..6bc16f3a714 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
}
diff --git a/unpack-trees.h b/unpack-trees.h
index 71ffb7eeb0c..efb9edfbb27 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -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,
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 04/11] unpack-trees: add special cwd handling
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (2 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
` (8 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 2 +-
unpack-trees.c | 13 +++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 55081f5c980..10e4654042c 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 6bc16f3a714..5852807d2fb 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 05/11] symlinks: do not include startup_info->original_cwd in dir removal
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (3 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (7 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
symlinks.c | 8 +++++++-
t/t2501-cwd-empty.sh | 10 +++++-----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/symlinks.c b/symlinks.c
index 5232d02020c..c667baa949b 100644
--- a/symlinks.c
+++ b/symlinks.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 10e4654042c..50619de10aa 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 06/11] clean: do not attempt to remove startup_info->original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (4 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 07/11] rebase: " Elijah Newren via GitGitGadget
` (6 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/clean.c | 44 +++++++++++++++++++++++++++++++++++---------
t/t2501-cwd-empty.sh | 5 +++--
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/builtin/clean.c b/builtin/clean.c
index 98a2860409b..3ff02bbbffe 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -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;
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 50619de10aa..6ab82e05d98 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (5 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 08/11] stash: " Elijah Newren via GitGitGadget
` (5 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
sequencer.c | 2 ++
t/t2501-cwd-empty.sh | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index ea96837cde3..83f257e7fa4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 6ab82e05d98..4f7dba9ab3d 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 08/11] stash: do not attempt to remove startup_info->original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (6 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
` (4 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 <newren@gmail.com>
---
builtin/stash.c | 4 +++-
t/t2501-cwd-empty.sh | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/builtin/stash.c b/builtin/stash.c
index a0ccc8654df..de0e432a4ff 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
struct 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)) {
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 4f7dba9ab3d..e2586a63eca 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 09/11] dir: avoid incidentally removing the original_cwd in remove_path()
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (7 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 08/11] stash: " Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
` (3 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
dir.c | 3 +++
dir.h | 6 +++++-
t/t2501-cwd-empty.sh | 12 ++++--------
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/dir.c b/dir.c
index 94489298f4c..97d6b71c872 100644
--- a/dir.c
+++ b/dir.c
@@ -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);
}
diff --git a/dir.h b/dir.h
index 83f46c0fb4c..d6a5d03bec2 100644
--- a/dir.h
+++ b/dir.h
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index e2586a63eca..30b8ffaa11b 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (8 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 22:37 ` [PATCH v4 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
` (2 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/rm.c | 3 ++-
dir.c | 12 +++++++++---
dir.h | 3 +++
t/t2501-cwd-empty.sh | 5 -----
4 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/builtin/rm.c b/builtin/rm.c
index 3d0967cdc11..b4132e5d8ee 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -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;
diff --git a/dir.c b/dir.c
index 97d6b71c872..52064345a6b 100644
--- a/dir.c
+++ b/dir.c
@@ -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.
diff --git a/dir.h b/dir.h
index d6a5d03bec2..8e02dfb505d 100644
--- a/dir.h
+++ b/dir.h
@@ -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.
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 30b8ffaa11b..6d8f68c08dd 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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" &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v4 11/11] t2501: simplify the tests since we can now assume desired behavior
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (9 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
@ 2021-11-29 22:37 ` Elijah Newren via GitGitGadget
2021-11-29 23:38 ` [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty Eric Sunshine
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-11-29 22:37 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
We no longer are dealing with a mixture of previous and desired
behavior, so simplify the tests a bit.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------
1 file changed, 36 insertions(+), 87 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 6d8f68c08dd..f6d8d7d03d7 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (10 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
@ 2021-11-29 23:38 ` Eric Sunshine
2021-11-30 0:16 ` Elijah Newren
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
12 siblings, 1 reply; 163+ messages in thread
From: Eric Sunshine @ 2021-11-29 23:38 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: Git List, Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee
On Mon, Nov 29, 2021 at 6:10 PM Elijah Newren via GitGitGadget
<gitgitgadget@gmail.com> wrote:
> Changes since v3:
> * fixed bashism
> -+ if [[ $works == "success" ]]; then
> ++ if test "$works" == "success"
This and any other instances in the series should be using POSIX `=`,
not Bash `==`.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-29 23:38 ` [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty Eric Sunshine
@ 2021-11-30 0:16 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-11-30 0:16 UTC (permalink / raw)
To: Eric Sunshine
Cc: Elijah Newren via GitGitGadget, Git List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley, Derrick Stolee
On Mon, Nov 29, 2021 at 3:38 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
>
> On Mon, Nov 29, 2021 at 6:10 PM Elijah Newren via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
> > Changes since v3:
> > * fixed bashism
> > -+ if [[ $works == "success" ]]; then
> > ++ if test "$works" == "success"
>
> This and any other instances in the series should be using POSIX `=`,
> not Bash `==`.
Ugh, I had them removed by the end of the series but somehow missed
checking the earlier patch. I'll fix it up; thanks for spotting.
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
` (11 preceding siblings ...)
2021-11-29 23:38 ` [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty Eric Sunshine
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
` (12 more replies)
12 siblings, 13 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren
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_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/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/
[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/
[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]
https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/
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-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v5
Pull-Request: https://github.com/git/git/pull/1140
Range-diff vs v4:
1: a45b3f08802 ! 1: 7c72e888d2e t2501: add various tests for removing the current working directory
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want pwd & git status to pass, test for existing
+ # rather than desired behavior.
-+ if test "$works" == "success"
++ if test "$works" = "success"
+ then
+ test-tool getcwd &&
+ git status --porcelain
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want dir to be present, test for existing rather
+ # than desired behavior.
-+ if test "$works" == "success"
++ if test "$works" = "success"
+ then
+ test_path_is_dir foo
+ else
@@ t/t2501-cwd-empty.sh (new)
+ # 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"
++ if test "$works" = "success"
+ then
+ test_must_fail "$@" 2>../error &&
+ grep "Refusing to remove.*current working directory" ../error &&
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want dirORfile to be present, test for existing rather
+ # than desired behavior.
-+ if test "$works" == "success"
++ if test "$works" = "success"
+ then
+ test_path_is_dir dirORfile
+ else
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want pwd & git status to pass, test for existing
+ # rather than desired behavior.
-+ if test "$works" == "success"
++ if test "$works" = "success"
+ then
+ test-tool getcwd &&
+ git status --porcelain
@@ t/t2501-cwd-empty.sh (new)
+
+ # Although we want dir to be present, test for existing rather
+ # than desired behavior.
-+ if test "$works" == "success"
++ if test "$works" = "success"
+ then
+ test_path_is_dir untracked
+ else
@@ t/t2501-cwd-empty.sh (new)
+ test_status=
+ test "$path_status" = dir && test_status=test_must_fail
+
-+ # Actually, while 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=
2: ca9f632bd11 = 2: 37f333b2024 setup: introduce startup_info->original_cwd
3: 41a82eff41e = 3: b611c73bd15 unpack-trees: refuse to remove startup_info->original_cwd
4: 2e2ea02f97b = 4: 706415a4547 unpack-trees: add special cwd handling
5: f444a541da4 = 5: 66ef6b4d943 symlinks: do not include startup_info->original_cwd in dir removal
6: 1990e36bb41 = 6: 54d059c683e clean: do not attempt to remove startup_info->original_cwd
7: 1035ee7f9ce = 7: f8efb7446c3 rebase: do not attempt to remove startup_info->original_cwd
8: a2be40a22d1 = 8: 2855ed45112 stash: do not attempt to remove startup_info->original_cwd
9: 834031be9e0 = 9: 23dfc3e399d dir: avoid incidentally removing the original_cwd in remove_path()
10: d5750fcb6d5 ! 10: fe47c0f0c17 dir: new flag to remove_dir_recurse() to spare the original_cwd
@@ 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
+- # Actually, while path_status=dir && test_status=test_must_fail
- # reflect our desired behavior, current behavior is:
- path_status=missing
- test_status=
11: 21ff99a767c ! 11: 431dd651a7e t2501: simplify the tests since we can now assume desired behavior
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # Although we want pwd & git status to pass, test for existing
- # rather than desired behavior.
-- if test "$works" == "success"
+- if test "$works" = "success"
- then
- test-tool getcwd &&
- git status --porcelain
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # Although we want dir to be present, test for existing rather
- # than desired behavior.
-- if test "$works" == "success"
+- if test "$works" = "success"
- then
- test_path_is_dir foo
- else
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # 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"
+- if test "$works" = "success"
- then
- test_must_fail "$@" 2>../error &&
- grep "Refusing to remove.*current working directory" ../error &&
@@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {
- # Although we want dirORfile to be present, test for existing rather
- # than desired behavior.
-- if test "$works" == "success"
+- if test "$works" = "success"
- then
- test_path_is_dir dirORfile
- else
@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {
- # Although we want pwd & git status to pass, test for existing
- # rather than desired behavior.
-- if test "$works" == "success"
+- if test "$works" = "success"
- then
- test-tool getcwd &&
- git status --porcelain
@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {
- # Although we want dir to be present, test for existing rather
- # than desired behavior.
-- if test "$works" == "success"
+- if test "$works" = "success"
- then
- test_path_is_dir untracked
- else
--
gitgitgadget
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v5 01/11] t2501: add various tests for removing the current working directory
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
` (11 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 342 insertions(+)
create mode 100755 t/t2501-cwd-empty.sh
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
new file mode 100755
index 00000000000..a05abd18187
--- /dev/null
+++ b/t/t2501-cwd-empty.sh
@@ -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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 02/11] setup: introduce startup_info->original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (10 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 directory
Non-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
hello
This 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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
cache.h | 2 ++
common-main.c | 4 ++++
setup.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 71 insertions(+)
diff --git a/cache.h b/cache.h
index eba12487b99..92e181ea759 100644
--- a/cache.h
+++ b/cache.h
@@ -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;
diff --git a/common-main.c b/common-main.c
index 71e21dd20a3..aa8d5aba5bb 100644
--- a/common-main.c
+++ b/common-main.c
@@ -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);
diff --git a/setup.c b/setup.c
index 347d7181ae9..af3b8c09abe 100644
--- a/setup.c
+++ b/setup.c
@@ -12,6 +12,7 @@ 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
@@ -432,6 +433,69 @@ void setup_work_tree(void)
initialized = 1;
}
+static void setup_original_cwd(void)
+{
+ struct strbuf tmp = STRBUF_INIT;
+ const char *worktree = NULL;
+ int offset = -1;
+
+ 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 removing.
+ *
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
+ *
+ * Yes, startup_info->original_cwd is usually the same as 'prefix',
+ * but differs in two ways:
+ * - prefix has a trailing '/'
+ * - if the user passes '-C' to git, that modifies the prefix but
+ * not startup_info->original_cwd.
+ */
+
+ /* Normalize the directory */
+ strbuf_realpath(&tmp, tmp_original_cwd, 1);
+ free((char*)tmp_original_cwd);
+ tmp_original_cwd = NULL;
+ startup_info->original_cwd = strbuf_detach(&tmp, NULL);
+
+ /*
+ * Get our worktree; we only protect the current working directory
+ * if it's in the worktree.
+ */
+ worktree = get_git_work_tree();
+ 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
+ */
+ startup_info->original_cwd = \
+ 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)
{
struct repository_format *data = vdata;
@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
}
+ setup_original_cwd();
strbuf_release(&dir);
strbuf_release(&gitdir);
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 03/11] unpack-trees: refuse to remove startup_info->original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
` (9 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 20 +++++++-------------
unpack-trees.c | 17 +++++++++++++----
unpack-trees.h | 1 +
3 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index a05abd18187..398908dfc93 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 89ca95ce90b..6bc16f3a714 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
}
diff --git a/unpack-trees.h b/unpack-trees.h
index 71ffb7eeb0c..efb9edfbb27 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -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,
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 04/11] unpack-trees: add special cwd handling
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (2 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
` (8 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 2 +-
unpack-trees.c | 13 +++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 398908dfc93..5af1fec6fec 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 6bc16f3a714..5852807d2fb 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 05/11] symlinks: do not include startup_info->original_cwd in dir removal
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (3 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (7 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
symlinks.c | 8 +++++++-
t/t2501-cwd-empty.sh | 10 +++++-----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/symlinks.c b/symlinks.c
index 5232d02020c..c667baa949b 100644
--- a/symlinks.c
+++ b/symlinks.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 5af1fec6fec..e4502d24d57 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 06/11] clean: do not attempt to remove startup_info->original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (4 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 07/11] rebase: " Elijah Newren via GitGitGadget
` (6 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/clean.c | 44 +++++++++++++++++++++++++++++++++++---------
t/t2501-cwd-empty.sh | 5 +++--
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/builtin/clean.c b/builtin/clean.c
index 98a2860409b..3ff02bbbffe 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -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;
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index e4502d24d57..b1182390ba3 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (5 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2022-01-25 20:26 ` [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd) Glen Choo
2021-12-01 6:40 ` [PATCH v5 08/11] stash: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (5 subsequent siblings)
12 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
sequencer.c | 2 ++
t/t2501-cwd-empty.sh | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index ea96837cde3..83f257e7fa4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index b1182390ba3..52335a8afe9 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2021-12-01 6:40 ` [PATCH v5 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2022-01-25 20:26 ` Glen Choo
2022-01-25 23:59 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Glen Choo @ 2022-01-25 20:26 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget, git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood
"Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Elijah Newren <newren@gmail.com>
>
> 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.
>
> Signed-off-by: Elijah Newren <newren@gmail.com>
> ---
> sequencer.c | 2 ++
> t/t2501-cwd-empty.sh | 4 ++--
> 2 files changed, 4 insertions(+), 2 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index ea96837cde3..83f257e7fa4 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -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);
> diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
> index b1182390ba3..52335a8afe9 100755
> --- a/t/t2501-cwd-empty.sh
> +++ b/t/t2501-cwd-empty.sh
> @@ -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' '
> --
> gitgitgadget
This commit (which is already in master) introduces a bug that breaks
rebase when rebasing inside a subdirectory of a worktree. You can see
that the below test fails with:
error: The following untracked working tree files would be overwritten by merge:
a/b/c
Please move or remove them before you merge.
This only affects subdirectories in worktrees, i.e. rebasing anywhere in
the `main-wt` directory is fine, and rebasing from the top of `other-wt`
is fine, but `other-wt/any/other/dir` fails.
I haven't tracked down the root cause yet, but judging from the commit,
I would suppose that the checkout is being spawned in the wrong
directory, causing the files to not be cleaned up.
---
t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
index 23dbd3c82e..8b8b66538b 100755
--- a/t/t3400-rebase.sh
+++ b/t/t3400-rebase.sh
@@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'
mv actual_logs .git/logs
'
+test_expect_success 'rebase when inside worktree subdirectory' '
+ git init main-wt &&
+ (
+ cd main-wt &&
+ git commit --allow-empty -m "initial" &&
+ # create commit with foo/bar/baz
+ mkdir -p foo/bar &&
+ touch foo/bar/baz &&
+ git add foo/bar/baz &&
+ git commit -m "add foo/bar/baz" &&
+ # create commit with a/b/c
+ mkdir -p a/b &&
+ touch a/b/c &&
+ git add a/b/c &&
+ git commit -m "add a/b/c" &&
+ # create another branch for our other worktree
+ git branch other &&
+ git worktree add ../other-wt other &&
+ (
+ cd ../other-wt &&
+ mkdir -p random/dir &&
+ (
+ cd random/dir &&
+ git rebase --onto HEAD^^ HEAD^ # drops the HEAD^ commit
+ )
+ )
+ )
+'
+
test_done
--
2.35.0.rc0.227.g00780c9af4-goog
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-25 20:26 ` [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd) Glen Choo
@ 2022-01-25 23:59 ` Elijah Newren
2022-01-26 0:30 ` Glen Choo
` (2 more replies)
0 siblings, 3 replies; 163+ messages in thread
From: Elijah Newren @ 2022-01-25 23:59 UTC (permalink / raw)
To: Glen Choo
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood
On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
>
> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > From: Elijah Newren <newren@gmail.com>
> >
> > 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.
> >
> > Signed-off-by: Elijah Newren <newren@gmail.com>
> > ---
> > sequencer.c | 2 ++
> > t/t2501-cwd-empty.sh | 4 ++--
> > 2 files changed, 4 insertions(+), 2 deletions(-)
> >
> > diff --git a/sequencer.c b/sequencer.c
> > index ea96837cde3..83f257e7fa4 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -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);
> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
> > index b1182390ba3..52335a8afe9 100755
> > --- a/t/t2501-cwd-empty.sh
> > +++ b/t/t2501-cwd-empty.sh
> > @@ -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' '
> > --
> > gitgitgadget
>
> This commit (which is already in master) introduces a bug that breaks
> rebase when rebasing inside a subdirectory of a worktree. You can see
> that the below test fails with:
>
> error: The following untracked working tree files would be overwritten by merge:
> a/b/c
> Please move or remove them before you merge.
Thanks for the detailed report -- with a full testcase!
> This only affects subdirectories in worktrees, i.e. rebasing anywhere in
> the `main-wt` directory is fine, and rebasing from the top of `other-wt`
> is fine, but `other-wt/any/other/dir` fails.
>
> I haven't tracked down the root cause yet, but judging from the commit,
> I would suppose that the checkout is being spawned in the wrong
> directory, causing the files to not be cleaned up.
There's nothing wrong with running checkout from a subdirectory. It
is unfortunate that setup.c auto-discovers both the git directory and
the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
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.
But, anyway, I've got a patch that I'll send as soon as it passes CI
(https://github.com/git/git/pull/1205).
> ---
> t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++
> 1 file changed, 29 insertions(+)
>
> diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
> index 23dbd3c82e..8b8b66538b 100755
> --- a/t/t3400-rebase.sh
> +++ b/t/t3400-rebase.sh
> @@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'
> mv actual_logs .git/logs
> '
>
> +test_expect_success 'rebase when inside worktree subdirectory' '
> + git init main-wt &&
> + (
> + cd main-wt &&
> + git commit --allow-empty -m "initial" &&
> + # create commit with foo/bar/baz
> + mkdir -p foo/bar &&
> + touch foo/bar/baz &&
> + git add foo/bar/baz &&
> + git commit -m "add foo/bar/baz" &&
> + # create commit with a/b/c
> + mkdir -p a/b &&
> + touch a/b/c &&
> + git add a/b/c &&
> + git commit -m "add a/b/c" &&
> + # create another branch for our other worktree
> + git branch other &&
> + git worktree add ../other-wt other &&
> + (
> + cd ../other-wt &&
> + mkdir -p random/dir &&
> + (
> + cd random/dir &&
> + git rebase --onto HEAD^^ HEAD^ # drops the HEAD^ commit
> + )
> + )
> + )
> +'
> +
> test_done
> --
> 2.35.0.rc0.227.g00780c9af4-goog
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-25 23:59 ` Elijah Newren
@ 2022-01-26 0:30 ` Glen Choo
2022-01-26 19:04 ` Elijah Newren
2022-01-26 0:32 ` Eric Sunshine
2022-01-26 11:00 ` Phillip Wood
2 siblings, 1 reply; 163+ messages in thread
From: Glen Choo @ 2022-01-26 0:30 UTC (permalink / raw)
To: Elijah Newren
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood
Elijah Newren <newren@gmail.com> writes:
> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
>>
>> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>
>> > From: Elijah Newren <newren@gmail.com>
>> >
>> > 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.
>> >
>> > Signed-off-by: Elijah Newren <newren@gmail.com>
>> > ---
>> > sequencer.c | 2 ++
>> > t/t2501-cwd-empty.sh | 4 ++--
>> > 2 files changed, 4 insertions(+), 2 deletions(-)
>> >
>> > diff --git a/sequencer.c b/sequencer.c
>> > index ea96837cde3..83f257e7fa4 100644
>> > --- a/sequencer.c
>> > +++ b/sequencer.c
>> > @@ -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);
>> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
>> > index b1182390ba3..52335a8afe9 100755
>> > --- a/t/t2501-cwd-empty.sh
>> > +++ b/t/t2501-cwd-empty.sh
>> > @@ -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' '
>> > --
>> > gitgitgadget
>>
>> This commit (which is already in master) introduces a bug that breaks
>> rebase when rebasing inside a subdirectory of a worktree. You can see
>> that the below test fails with:
>>
>> error: The following untracked working tree files would be overwritten by merge:
>> a/b/c
>> Please move or remove them before you merge.
>
> Thanks for the detailed report -- with a full testcase!
Glad to be of help :)
>> This only affects subdirectories in worktrees, i.e. rebasing anywhere in
>> the `main-wt` directory is fine, and rebasing from the top of `other-wt`
>> is fine, but `other-wt/any/other/dir` fails.
>>
>> I haven't tracked down the root cause yet, but judging from the commit,
>> I would suppose that the checkout is being spawned in the wrong
>> directory, causing the files to not be cleaned up.
>
> There's nothing wrong with running checkout from a subdirectory. It
> is unfortunate that setup.c auto-discovers both the git directory and
> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> 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.
>
> But, anyway, I've got a patch that I'll send as soon as it passes CI
> (https://github.com/git/git/pull/1205).
I wish I had seen this email before diving into debugging it myself
since your fix is more comprehensive, but it was a good learning
experience nonetheless.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 0:30 ` Glen Choo
@ 2022-01-26 19:04 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2022-01-26 19:04 UTC (permalink / raw)
To: Glen Choo
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood
On Tue, Jan 25, 2022 at 4:30 PM Glen Choo <chooglen@google.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
> >>
> >> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> >>
> >> > From: Elijah Newren <newren@gmail.com>
> >> >
> >> > 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.
> >> >
> >> > Signed-off-by: Elijah Newren <newren@gmail.com>
> >> > ---
> >> > sequencer.c | 2 ++
> >> > t/t2501-cwd-empty.sh | 4 ++--
> >> > 2 files changed, 4 insertions(+), 2 deletions(-)
> >> >
> >> > diff --git a/sequencer.c b/sequencer.c
> >> > index ea96837cde3..83f257e7fa4 100644
> >> > --- a/sequencer.c
> >> > +++ b/sequencer.c
> >> > @@ -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);
> >> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
> >> > index b1182390ba3..52335a8afe9 100755
> >> > --- a/t/t2501-cwd-empty.sh
> >> > +++ b/t/t2501-cwd-empty.sh
> >> > @@ -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' '
> >> > --
> >> > gitgitgadget
> >>
> >> This commit (which is already in master) introduces a bug that breaks
> >> rebase when rebasing inside a subdirectory of a worktree. You can see
> >> that the below test fails with:
> >>
> >> error: The following untracked working tree files would be overwritten by merge:
> >> a/b/c
> >> Please move or remove them before you merge.
> >
> > Thanks for the detailed report -- with a full testcase!
>
> Glad to be of help :)
>
> >> This only affects subdirectories in worktrees, i.e. rebasing anywhere in
> >> the `main-wt` directory is fine, and rebasing from the top of `other-wt`
> >> is fine, but `other-wt/any/other/dir` fails.
> >>
> >> I haven't tracked down the root cause yet, but judging from the commit,
> >> I would suppose that the checkout is being spawned in the wrong
> >> directory, causing the files to not be cleaned up.
> >
> > There's nothing wrong with running checkout from a subdirectory. It
> > is unfortunate that setup.c auto-discovers both the git directory and
> > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> > 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.
> >
> > But, anyway, I've got a patch that I'll send as soon as it passes CI
> > (https://github.com/git/git/pull/1205).
>
> I wish I had seen this email before diving into debugging it myself
> since your fix is more comprehensive, but it was a good learning
> experience nonetheless.
Yeah, sorry, it took me quite a bit of debugging as well before
figuring all this stuff out, so I don't think I sent the email in time
to save you the work.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-25 23:59 ` Elijah Newren
2022-01-26 0:30 ` Glen Choo
@ 2022-01-26 0:32 ` Eric Sunshine
2022-01-26 0:38 ` Eric Sunshine
2022-01-26 11:00 ` Phillip Wood
2 siblings, 1 reply; 163+ messages in thread
From: Eric Sunshine @ 2022-01-26 0:32 UTC (permalink / raw)
To: Elijah Newren
Cc: Glen Choo, Elijah Newren via GitGitGadget, Git Mailing List,
Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee, Phillip Wood
On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:
> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
> > This commit (which is already in master) introduces a bug that breaks
> > rebase when rebasing inside a subdirectory of a worktree. You can see
> > that the below test fails with:
>
> There's nothing wrong with running checkout from a subdirectory. It
> is unfortunate that setup.c auto-discovers both the git directory and
> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> 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.
>
> But, anyway, I've got a patch that I'll send as soon as it passes CI
> (https://github.com/git/git/pull/1205).
>
> > +test_expect_success 'rebase when inside worktree subdirectory' '
> > + git init main-wt &&
> > + (
> > + cd main-wt &&
> > + git commit --allow-empty -m "initial" &&
> > + # create commit with foo/bar/baz
> > + mkdir -p foo/bar &&
> > + touch foo/bar/baz &&
> > + git add foo/bar/baz &&
> > + git commit -m "add foo/bar/baz" &&
> > + # create commit with a/b/c
> > + mkdir -p a/b &&
> > + touch a/b/c &&
> > + git add a/b/c &&
> > + git commit -m "add a/b/c" &&
> > + # create another branch for our other worktree
> > + git branch other &&
> > + git worktree add ../other-wt other &&
> > + (
> > + cd ../other-wt &&
> > + mkdir -p random/dir &&
> > + (
> > + cd random/dir &&
> > + git rebase --onto HEAD^^ HEAD^ # drops the HEAD^ commit
> > + )
> > + )
> > + )
> > +'
This is entirely minor, but all the inner subshells in this test are
superfluous. The outermost (...) will ensure that the working
directory is restored regardless of whether anything within its body
fails, no matter how much you `cd` around. Thus, all the inner
subshells could be dropped.
An alternative would be to close the outermost subshell immediately
after `git worktree add`:
git init main-wt &&
(
cd main-wt &&
...
git worktree add ../other-wt other
) &&
mkdir -p other-wt/random/dir &&
(
cd other-wt/random/dir &&
...
)
however, that may not buy you any clarity.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 0:32 ` Eric Sunshine
@ 2022-01-26 0:38 ` Eric Sunshine
2022-01-26 0:51 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Eric Sunshine @ 2022-01-26 0:38 UTC (permalink / raw)
To: Elijah Newren
Cc: Glen Choo, Elijah Newren via GitGitGadget, Git Mailing List,
Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee, Phillip Wood
On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
> On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:
> > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
> > > +test_expect_success 'rebase when inside worktree subdirectory' '
> > > + git init main-wt &&
> > > + (
> > > + cd main-wt &&
> > > + git commit --allow-empty -m "initial" &&
> > > + # create commit with foo/bar/baz
> > > + mkdir -p foo/bar &&
> > > + touch foo/bar/baz &&
> > > + git add foo/bar/baz &&
> > > + git commit -m "add foo/bar/baz" &&
> > > + # create commit with a/b/c
> > > + mkdir -p a/b &&
> > > + touch a/b/c &&
> > > + git add a/b/c &&
> > > + git commit -m "add a/b/c" &&
>
> This is entirely minor, but all the inner subshells in this test are
> superfluous. [...]
One other minor comment: If the file's timestamp has no significance
to the test, then our style is to create the file with `>` rather than
`touch`, so:
... &&
>foo/bar/baz &&
...
>a/b/c &&
...
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 0:38 ` Eric Sunshine
@ 2022-01-26 0:51 ` Elijah Newren
2022-01-26 1:15 ` Glen Choo
0 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren @ 2022-01-26 0:51 UTC (permalink / raw)
To: Eric Sunshine
Cc: Glen Choo, Elijah Newren via GitGitGadget, Git Mailing List,
Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee, Phillip Wood
On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
>
> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:
> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
> > > > +test_expect_success 'rebase when inside worktree subdirectory' '
> > > > + git init main-wt &&
> > > > + (
> > > > + cd main-wt &&
> > > > + git commit --allow-empty -m "initial" &&
> > > > + # create commit with foo/bar/baz
> > > > + mkdir -p foo/bar &&
> > > > + touch foo/bar/baz &&
> > > > + git add foo/bar/baz &&
> > > > + git commit -m "add foo/bar/baz" &&
> > > > + # create commit with a/b/c
> > > > + mkdir -p a/b &&
> > > > + touch a/b/c &&
> > > > + git add a/b/c &&
> > > > + git commit -m "add a/b/c" &&
> >
> > This is entirely minor, but all the inner subshells in this test are
> > superfluous. [...]
>
> One other minor comment: If the file's timestamp has no significance
> to the test, then our style is to create the file with `>` rather than
> `touch`, so:
>
> ... &&
> >foo/bar/baz &&
> ...
> >a/b/c &&
> ...
Ah, good point. And while at it, we can replace the touch/add/commit
sequence with a simple test_commit call.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 0:51 ` Elijah Newren
@ 2022-01-26 1:15 ` Glen Choo
2022-01-26 1:38 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Glen Choo @ 2022-01-26 1:15 UTC (permalink / raw)
To: Elijah Newren, Eric Sunshine
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Phillip Wood
Elijah Newren <newren@gmail.com> writes:
> On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
>>
>> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
>> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:
>> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
>> > > > +test_expect_success 'rebase when inside worktree subdirectory' '
>> > > > + git init main-wt &&
>> > > > + (
>> > > > + cd main-wt &&
>> > > > + git commit --allow-empty -m "initial" &&
>> > > > + # create commit with foo/bar/baz
>> > > > + mkdir -p foo/bar &&
>> > > > + touch foo/bar/baz &&
>> > > > + git add foo/bar/baz &&
>> > > > + git commit -m "add foo/bar/baz" &&
>> > > > + # create commit with a/b/c
>> > > > + mkdir -p a/b &&
>> > > > + touch a/b/c &&
>> > > > + git add a/b/c &&
>> > > > + git commit -m "add a/b/c" &&
>> >
>> > This is entirely minor, but all the inner subshells in this test are
>> > superfluous. [...]
>>
>> One other minor comment: If the file's timestamp has no significance
>> to the test, then our style is to create the file with `>` rather than
>> `touch`, so:
>>
>> ... &&
>> >foo/bar/baz &&
>> ...
>> >a/b/c &&
>> ...
>
> Ah, good point. And while at it, we can replace the touch/add/commit
> sequence with a simple test_commit call.
Yes, thanks for pointing that out - I had copied my reproduction script
into the test case without paying enough attention.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 1:15 ` Glen Choo
@ 2022-01-26 1:38 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2022-01-26 1:38 UTC (permalink / raw)
To: Glen Choo
Cc: Eric Sunshine, Elijah Newren via GitGitGadget, Git Mailing List,
Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee, Phillip Wood
On Tue, Jan 25, 2022 at 5:15 PM Glen Choo <chooglen@google.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
> >>
> >> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:
> >> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:
> >> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
> >> > > > +test_expect_success 'rebase when inside worktree subdirectory' '
> >> > > > + git init main-wt &&
> >> > > > + (
> >> > > > + cd main-wt &&
> >> > > > + git commit --allow-empty -m "initial" &&
> >> > > > + # create commit with foo/bar/baz
> >> > > > + mkdir -p foo/bar &&
> >> > > > + touch foo/bar/baz &&
> >> > > > + git add foo/bar/baz &&
> >> > > > + git commit -m "add foo/bar/baz" &&
> >> > > > + # create commit with a/b/c
> >> > > > + mkdir -p a/b &&
> >> > > > + touch a/b/c &&
> >> > > > + git add a/b/c &&
> >> > > > + git commit -m "add a/b/c" &&
> >> >
> >> > This is entirely minor, but all the inner subshells in this test are
> >> > superfluous. [...]
> >>
> >> One other minor comment: If the file's timestamp has no significance
> >> to the test, then our style is to create the file with `>` rather than
> >> `touch`, so:
> >>
> >> ... &&
> >> >foo/bar/baz &&
> >> ...
> >> >a/b/c &&
> >> ...
> >
> > Ah, good point. And while at it, we can replace the touch/add/commit
> > sequence with a simple test_commit call.
>
> Yes, thanks for pointing that out - I had copied my reproduction script
> into the test case without paying enough attention.
To be honest, it's the kind of thing others have reminded me of dozens
of times. In fact, although I read your testcase, it didn't even
occur to me until Eric brought up ">" instead of "touch" (which was
something else I had been reminded of dozens of times before
learning).
To me, it's all small potatoes compared to coming up with a good
reproducible testcase; that was the hard work, and you did that. You
even found a fix and called out the right things in the review for
others to look at. So, it's all good.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-25 23:59 ` Elijah Newren
2022-01-26 0:30 ` Glen Choo
2022-01-26 0:32 ` Eric Sunshine
@ 2022-01-26 11:00 ` Phillip Wood
2022-01-26 17:53 ` Eric Sunshine
2 siblings, 1 reply; 163+ messages in thread
From: Phillip Wood @ 2022-01-26 11:00 UTC (permalink / raw)
To: Elijah Newren, Glen Choo
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Eric Sunshine
On 25/01/2022 23:59, Elijah Newren wrote:
> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:
>>
>> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>
>>> From: Elijah Newren <newren@gmail.com>
>>>
>>> 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.
>>>
>>> Signed-off-by: Elijah Newren <newren@gmail.com>
>>> ---
>>> sequencer.c | 2 ++
>>> t/t2501-cwd-empty.sh | 4 ++--
>>> 2 files changed, 4 insertions(+), 2 deletions(-)
>>>
>>> diff --git a/sequencer.c b/sequencer.c
>>> index ea96837cde3..83f257e7fa4 100644
>>> --- a/sequencer.c
>>> +++ b/sequencer.c
>>> @@ -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);
>>> diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
>>> index b1182390ba3..52335a8afe9 100755
>>> --- a/t/t2501-cwd-empty.sh
>>> +++ b/t/t2501-cwd-empty.sh
>>> @@ -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' '
>>> --
>>> gitgitgadget
>>
>> This commit (which is already in master) introduces a bug that breaks
>> rebase when rebasing inside a subdirectory of a worktree. You can see
>> that the below test fails with:
>>
>> error: The following untracked working tree files would be overwritten by merge:
>> a/b/c
>> Please move or remove them before you merge.
>
> Thanks for the detailed report -- with a full testcase!
>
>> This only affects subdirectories in worktrees, i.e. rebasing anywhere in
>> the `main-wt` directory is fine, and rebasing from the top of `other-wt`
>> is fine, but `other-wt/any/other/dir` fails.
>>
>> I haven't tracked down the root cause yet, but judging from the commit,
>> I would suppose that the checkout is being spawned in the wrong
>> directory, causing the files to not be cleaned up.
>
> There's nothing wrong with running checkout from a subdirectory. It
> is unfortunate that setup.c auto-discovers both the git directory and
> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> 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.
>
> 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_DIR and GIT_WORK_TREE when we were
in a non-main worktree but obviously we don't. Eric do you happen to
know if that is intentional?
As an aside I'm going to post an updated version of my series removing
the call to 'git checkout' from sequencer.c later today but we
definitely want to have this fix before that series gets merged.
Best Wishes
Phillip
>> ---
>> t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++
>> 1 file changed, 29 insertions(+)
>>
>> diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
>> index 23dbd3c82e..8b8b66538b 100755
>> --- a/t/t3400-rebase.sh
>> +++ b/t/t3400-rebase.sh
>> @@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'
>> mv actual_logs .git/logs
>> '
>>
>> +test_expect_success 'rebase when inside worktree subdirectory' '
>> + git init main-wt &&
>> + (
>> + cd main-wt &&
>> + git commit --allow-empty -m "initial" &&
>> + # create commit with foo/bar/baz
>> + mkdir -p foo/bar &&
>> + touch foo/bar/baz &&
>> + git add foo/bar/baz &&
>> + git commit -m "add foo/bar/baz" &&
>> + # create commit with a/b/c
>> + mkdir -p a/b &&
>> + touch a/b/c &&
>> + git add a/b/c &&
>> + git commit -m "add a/b/c" &&
>> + # create another branch for our other worktree
>> + git branch other &&
>> + git worktree add ../other-wt other &&
>> + (
>> + cd ../other-wt &&
>> + mkdir -p random/dir &&
>> + (
>> + cd random/dir &&
>> + git rebase --onto HEAD^^ HEAD^ # drops the HEAD^ commit
>> + )
>> + )
>> + )
>> +'
>> +
>> test_done
>> --
>> 2.35.0.rc0.227.g00780c9af4-goog
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 11:00 ` Phillip Wood
@ 2022-01-26 17:53 ` Eric Sunshine
2022-01-27 11:01 ` Phillip Wood
2022-01-27 20:03 ` Elijah Newren
0 siblings, 2 replies; 163+ messages in thread
From: Eric Sunshine @ 2022-01-26 17:53 UTC (permalink / raw)
To: Phillip Wood
Cc: Elijah Newren, Glen Choo, Elijah Newren via GitGitGadget,
Git Mailing List, Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee
On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 25/01/2022 23:59, Elijah Newren wrote:
> > There's nothing wrong with running checkout from a subdirectory. It
> > is unfortunate that setup.c auto-discovers both the git directory and
> > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> > 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.
> >
> > 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_DIR and GIT_WORK_TREE when we were
> in a non-main worktree but obviously we don't. Eric do you happen to
> know if that is intentional?
As far as I know, there is no reason to set GIT_DIR and GIT_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.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 17:53 ` Eric Sunshine
@ 2022-01-27 11:01 ` Phillip Wood
2022-01-27 20:03 ` Elijah Newren
1 sibling, 0 replies; 163+ messages in thread
From: Phillip Wood @ 2022-01-27 11:01 UTC (permalink / raw)
To: Eric Sunshine, Phillip Wood
Cc: Elijah Newren, Glen Choo, Elijah Newren via GitGitGadget,
Git Mailing List, Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Philip Oakley,
Derrick Stolee
On 26/01/2022 17:53, Eric Sunshine wrote:
> On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 25/01/2022 23:59, Elijah Newren wrote:
>>> There's nothing wrong with running checkout from a subdirectory. It
>>> is unfortunate that setup.c auto-discovers both the git directory and
>>> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
>>> 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.
>>>
>>> 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_DIR and GIT_WORK_TREE when we were
>> in a non-main worktree but obviously we don't. Eric do you happen to
>> know if that is intentional?
>
> As far as I know, there is no reason to set GIT_DIR and GIT_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 of course I should have thought of that. Now I'm wondering why we're
exporting GIT_DIR - I should go and read the code for myself.
Thanks
Phillip
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-26 17:53 ` Eric Sunshine
2022-01-27 11:01 ` Phillip Wood
@ 2022-01-27 20:03 ` Elijah Newren
2022-02-05 11:23 ` Eric Sunshine
1 sibling, 1 reply; 163+ messages in thread
From: Elijah Newren @ 2022-01-27 20:03 UTC (permalink / raw)
To: Eric Sunshine
Cc: Phillip Wood, Glen Choo, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee, Elijah Newren
On Wed, Jan 26, 2022 at 9:53 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
>
> On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> > On 25/01/2022 23:59, Elijah Newren wrote:
> > > There's nothing wrong with running checkout from a subdirectory. It
> > > is unfortunate that setup.c auto-discovers both the git directory and
> > > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in
> > > 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.
> > >
> > > 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_DIR and GIT_WORK_TREE when we were
> > in a non-main worktree but obviously we don't. Eric do you happen to
> > know if that is intentional?
>
> As far as I know, there is no reason to set GIT_DIR and GIT_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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
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(-)
diff --git a/builtin/clone.c b/builtin/clone.c
index 727e16e0ae..5304939cec 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -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);
}
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 9f4a0b816c..b5d2633b78 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -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";
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 8480a59f57..1d578d48a1 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -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);
diff --git a/bundle.c b/bundle.c
index a0bb687b0f..897d36c83a 100644
--- a/bundle.c
+++ b/bundle.c
@@ -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);
diff --git a/connected.c b/connected.c
index ed3025e7a2..3dfc6278e8 100644
--- a/connected.c
+++ b/connected.c
@@ -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;
diff --git a/environment.c b/environment.c
index fd0501e77a..db31c37a62 100644
--- a/environment.c
+++ b/environment.c
@@ -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);
}
diff --git a/fetch-pack.c b/fetch-pack.c
index dd6ec449f2..8b59014c3a 100644
--- a/fetch-pack.c
+++ b/fetch-pack.c
@@ -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;
diff --git a/run-command.c b/run-command.c
index 69dde42f1e..0fb21f92bf 100644
--- a/run-command.c
+++ b/run-command.c
@@ -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;
diff --git a/t/helper/test-subprocess.c b/t/helper/test-subprocess.c
index ff22f2fa2c..d58bf7e18d 100644
--- a/t/helper/test-subprocess.c
+++ b/t/helper/test-subprocess.c
@@ -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);
}
diff --git a/transport-helper.c b/transport-helper.c
index a0297b0986..d26b5ecd10 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -448,6 +448,9 @@ static int get_importer(struct transport *transport, struct child_process *fasti
strvec_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;
diff --git a/upload-pack.c b/upload-pack.c
index 8acc98741b..7bf36d6d3a 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -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)
--
2.35.0.2.g9420af4777.dirty
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-01-27 20:03 ` Elijah Newren
@ 2022-02-05 11:23 ` Eric Sunshine
2022-02-05 11:42 ` Eric Sunshine
0 siblings, 1 reply; 163+ messages in thread
From: Eric Sunshine @ 2022-02-05 11:23 UTC (permalink / raw)
To: Elijah Newren
Cc: Phillip Wood, Glen Choo, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee
On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:
> On Wed, Jan 26, 2022 at 9:53 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> > As far as I know, there is no reason to set GIT_DIR and GIT_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?
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...
> -- >8 --
> Subject: [RFC/POC PATCH] setup: do not pre-emptively set GIT_DIR based on discovery
>
> 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
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.
> Signed-off-by: Elijah Newren <newren@gmail.com>
> ---
> diff --git a/builtin/clone.c b/builtin/clone.c
> @@ -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`.
> diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
> @@ -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.
> diff --git a/environment.c b/environment.c
> @@ -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);
> }
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-02-05 11:23 ` Eric Sunshine
@ 2022-02-05 11:42 ` Eric Sunshine
2022-02-05 22:35 ` Elijah Newren
0 siblings, 1 reply; 163+ messages in thread
From: Eric Sunshine @ 2022-02-05 11:42 UTC (permalink / raw)
To: Elijah Newren
Cc: Phillip Wood, Glen Choo, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee
On Sat, Feb 5, 2022 at 6:23 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:
> > 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
>
> 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.
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.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)
2022-02-05 11:42 ` Eric Sunshine
@ 2022-02-05 22:35 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2022-02-05 22:35 UTC (permalink / raw)
To: Eric Sunshine
Cc: Phillip Wood, Glen Choo, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Philip Oakley, Derrick Stolee
On Sat, Feb 5, 2022 at 3:42 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
>
> On Sat, Feb 5, 2022 at 6:23 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> > On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:
> > > 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
> >
> > 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.
>
> 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.
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.
> 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.
Yes, well put.
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v5 08/11] stash: do not attempt to remove startup_info->original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (6 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
` (4 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 <newren@gmail.com>
---
builtin/stash.c | 4 +++-
t/t2501-cwd-empty.sh | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/builtin/stash.c b/builtin/stash.c
index a0ccc8654df..de0e432a4ff 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
struct 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)) {
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 52335a8afe9..be9ef903bd4 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 09/11] dir: avoid incidentally removing the original_cwd in remove_path()
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (7 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 08/11] stash: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
` (3 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
dir.c | 3 +++
dir.h | 6 +++++-
t/t2501-cwd-empty.sh | 12 ++++--------
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/dir.c b/dir.c
index 94489298f4c..97d6b71c872 100644
--- a/dir.c
+++ b/dir.c
@@ -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);
}
diff --git a/dir.h b/dir.h
index 83f46c0fb4c..d6a5d03bec2 100644
--- a/dir.h
+++ b/dir.h
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index be9ef903bd4..ce2efb9d30a 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (8 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-01 6:40 ` [PATCH v5 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
` (2 subsequent siblings)
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/rm.c | 3 ++-
dir.c | 12 +++++++++---
dir.h | 3 +++
t/t2501-cwd-empty.sh | 5 -----
4 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/builtin/rm.c b/builtin/rm.c
index 3d0967cdc11..b4132e5d8ee 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -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;
diff --git a/dir.c b/dir.c
index 97d6b71c872..52064345a6b 100644
--- a/dir.c
+++ b/dir.c
@@ -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.
diff --git a/dir.h b/dir.h
index d6a5d03bec2..8e02dfb505d 100644
--- a/dir.h
+++ b/dir.h
@@ -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.
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index ce2efb9d30a..bc92230f2f2 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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" &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v5 11/11] t2501: simplify the tests since we can now assume desired behavior
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (9 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
@ 2021-12-01 6:40 ` Elijah Newren via GitGitGadget
2021-12-07 16:09 ` [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
12 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-01 6:40 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
We no longer are dealing with a mixture of previous and desired
behavior, so simplify the tests a bit.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------
1 file changed, 36 insertions(+), 87 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index bc92230f2f2..f6d8d7d03d7 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (10 preceding siblings ...)
2021-12-01 6:40 ` [PATCH v5 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
@ 2021-12-07 16:09 ` Derrick Stolee
2021-12-07 18:30 ` Ævar Arnfjörð Bjarmason
2021-12-07 20:43 ` Elijah Newren
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
12 siblings, 2 replies; 163+ messages in thread
From: Derrick Stolee @ 2021-12-07 16:09 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget, git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Eric Sunshine, Phillip Wood
On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:
> 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_cwd as 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
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-07 16:09 ` [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
@ 2021-12-07 18:30 ` Ævar Arnfjörð Bjarmason
2021-12-07 20:57 ` Derrick Stolee
2021-12-07 20:43 ` Elijah Newren
1 sibling, 1 reply; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-12-07 18:30 UTC (permalink / raw)
To: Derrick Stolee
Cc: Elijah Newren via GitGitGadget, git, Jeff King,
René Scharfe, Elijah Newren, Glen Choo, Philip Oakley,
Eric Sunshine, Phillip Wood
On Tue, Dec 07 2021, Derrick Stolee wrote:
> On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:
>> 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_cwd as 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.
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... :)
1. https://lore.kernel.org/git/211207.86ee6opy0f.gmgdl@evledraar.gmail.com/
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-07 18:30 ` Ævar Arnfjörð Bjarmason
@ 2021-12-07 20:57 ` Derrick Stolee
2021-12-08 10:23 ` Ævar Arnfjörð Bjarmason
0 siblings, 1 reply; 163+ messages in thread
From: Derrick Stolee @ 2021-12-07 20:57 UTC (permalink / raw)
To: Ævar Arnfjörð Bjarmason
Cc: Elijah Newren via GitGitGadget, git, Jeff King,
René Scharfe, Elijah Newren, Glen Choo, Philip Oakley,
Eric Sunshine, Phillip Wood
On 12/7/2021 1:30 PM, Ævar Arnfjörð Bjarmason wrote:
>
> On Tue, Dec 07 2021, Derrick Stolee wrote:
>> 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.
>
> 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.
> 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?
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.
> 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.
Exactly. We should take this change because it is valuable to not cause
a confusing error in other tools.
> 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
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-07 20:57 ` Derrick Stolee
@ 2021-12-08 10:23 ` Ævar Arnfjörð Bjarmason
0 siblings, 0 replies; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-12-08 10:23 UTC (permalink / raw)
To: Derrick Stolee
Cc: Elijah Newren via GitGitGadget, git, Jeff King,
René Scharfe, Elijah Newren, Glen Choo, Philip Oakley,
Eric Sunshine, Phillip Wood
On Tue, Dec 07 2021, Derrick Stolee wrote:
> On 12/7/2021 1:30 PM, Ævar Arnfjörð Bjarmason wrote:
>>
>> On Tue, Dec 07 2021, Derrick Stolee wrote:
>>> 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.
>>
>> 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.
I'm asking whether the WIP patch I posted at
https://lore.kernel.org/git/211124.86sfvld4cl.gmgdl@evledraar.gmail.com/
would partially/entirely solve those issues you mentiened (but I see
Elijah has a side-reply saying it might not in either approach).
>> 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?
>
> 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...
>> 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.
>
> 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.
>> 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 :)
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-07 16:09 ` [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
2021-12-07 18:30 ` Ævar Arnfjörð Bjarmason
@ 2021-12-07 20:43 ` Elijah Newren
2021-12-07 21:00 ` Derrick Stolee
1 sibling, 1 reply; 163+ messages in thread
From: Elijah Newren @ 2021-12-07 20:43 UTC (permalink / raw)
To: Derrick Stolee
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley, Eric Sunshine, Phillip Wood
On Tue, Dec 7, 2021 at 8:09 AM Derrick Stolee <stolee@gmail.com> wrote:
>
> On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:
> > 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_cwd as 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)
Interesting testcase...
> 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.
> 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.
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-07 20:43 ` Elijah Newren
@ 2021-12-07 21:00 ` Derrick Stolee
0 siblings, 0 replies; 163+ messages in thread
From: Derrick Stolee @ 2021-12-07 21:00 UTC (permalink / raw)
To: Elijah Newren
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley, Eric Sunshine, Phillip Wood
On 12/7/2021 3:43 PM, Elijah Newren wrote:
> On Tue, Dec 7, 2021 at 8:09 AM Derrick Stolee <stolee@gmail.com> wrote:
>> 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...
>
>> 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?
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.
> 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.
>> 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.
Absolutely. Let's pick up this fix another time.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v6 00/11] Avoid removing the current working directory, even if it becomes empty
2021-12-01 6:40 ` [PATCH v5 " Elijah Newren via GitGitGadget
` (11 preceding siblings ...)
2021-12-07 16:09 ` [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty Derrick Stolee
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
` (10 more replies)
12 siblings, 11 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren
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_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/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/
[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/
[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]
https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/
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/aa85e35d-143e-93e4-f54b-146b38dd4b88@gmail.com/)
* from Ævar ("I've got no objections to these changes going in as they
stand.",
https://lore.kernel.org/git/211207.86ee6opy0f.gmgdl@evledraar.gmail.com/)
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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## 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 <stolee@gmail.com>
+ Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
## t/t2501-cwd-empty.sh ##
--
gitgitgadget
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v6 01/11] t2501: add various tests for removing the current working directory
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2022-03-11 11:57 ` Ævar Arnfjörð Bjarmason
2021-12-09 5:08 ` [PATCH v6 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
` (9 subsequent siblings)
10 siblings, 1 reply; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 342 insertions(+)
create mode 100755 t/t2501-cwd-empty.sh
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
new file mode 100755
index 00000000000..a05abd18187
--- /dev/null
+++ b/t/t2501-cwd-empty.sh
@@ -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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v6 01/11] t2501: add various tests for removing the current working directory
2021-12-09 5:08 ` [PATCH v6 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2022-03-11 11:57 ` Ævar Arnfjörð Bjarmason
0 siblings, 0 replies; 163+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-03-11 11:57 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget
Cc: git, Jeff King, René Scharfe, Glen Choo, Philip Oakley,
Derrick Stolee, Eric Sunshine, Phillip Wood, Elijah Newren
On Thu, Dec 09 2021, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
> [...]
> +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 :)
^ permalink raw reply [flat|nested] 163+ messages in thread
* [PATCH v6 02/11] setup: introduce startup_info->original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (8 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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 directory
Non-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
hello
This 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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
cache.h | 2 ++
common-main.c | 4 ++++
setup.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 71 insertions(+)
diff --git a/cache.h b/cache.h
index eba12487b99..92e181ea759 100644
--- a/cache.h
+++ b/cache.h
@@ -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;
diff --git a/common-main.c b/common-main.c
index 71e21dd20a3..aa8d5aba5bb 100644
--- a/common-main.c
+++ b/common-main.c
@@ -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);
diff --git a/setup.c b/setup.c
index 347d7181ae9..af3b8c09abe 100644
--- a/setup.c
+++ b/setup.c
@@ -12,6 +12,7 @@ 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
@@ -432,6 +433,69 @@ void setup_work_tree(void)
initialized = 1;
}
+static void setup_original_cwd(void)
+{
+ struct strbuf tmp = STRBUF_INIT;
+ const char *worktree = NULL;
+ int offset = -1;
+
+ 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 removing.
+ *
+ * For convience, we would like to have the path relative to the
+ * worktree instead of an absolute path.
+ *
+ * Yes, startup_info->original_cwd is usually the same as 'prefix',
+ * but differs in two ways:
+ * - prefix has a trailing '/'
+ * - if the user passes '-C' to git, that modifies the prefix but
+ * not startup_info->original_cwd.
+ */
+
+ /* Normalize the directory */
+ strbuf_realpath(&tmp, tmp_original_cwd, 1);
+ free((char*)tmp_original_cwd);
+ tmp_original_cwd = NULL;
+ startup_info->original_cwd = strbuf_detach(&tmp, NULL);
+
+ /*
+ * Get our worktree; we only protect the current working directory
+ * if it's in the worktree.
+ */
+ worktree = get_git_work_tree();
+ 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
+ */
+ startup_info->original_cwd = \
+ 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)
{
struct repository_format *data = vdata;
@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
}
+ setup_original_cwd();
strbuf_release(&dir);
strbuf_release(&gitdir);
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 03/11] unpack-trees: refuse to remove startup_info->original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 01/11] t2501: add various tests for removing the current working directory Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 02/11] setup: introduce startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
` (7 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 20 +++++++-------------
unpack-trees.c | 17 +++++++++++++----
unpack-trees.h | 1 +
3 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index a05abd18187..398908dfc93 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 89ca95ce90b..6bc16f3a714 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
}
diff --git a/unpack-trees.h b/unpack-trees.h
index 71ffb7eeb0c..efb9edfbb27 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -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,
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 04/11] unpack-trees: add special cwd handling
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (2 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 03/11] unpack-trees: refuse to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
` (6 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 2 +-
unpack-trees.c | 13 +++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 398908dfc93..5af1fec6fec 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
diff --git a/unpack-trees.c b/unpack-trees.c
index 6bc16f3a714..5852807d2fb 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 05/11] symlinks: do not include startup_info->original_cwd in dir removal
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (3 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 04/11] unpack-trees: add special cwd handling Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
` (5 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
symlinks.c | 8 +++++++-
t/t2501-cwd-empty.sh | 10 +++++-----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/symlinks.c b/symlinks.c
index 5232d02020c..c667baa949b 100644
--- a/symlinks.c
+++ b/symlinks.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 5af1fec6fec..e4502d24d57 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 06/11] clean: do not attempt to remove startup_info->original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (4 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 05/11] symlinks: do not include startup_info->original_cwd in dir removal Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 07/11] rebase: " Elijah Newren via GitGitGadget
` (4 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/clean.c | 44 +++++++++++++++++++++++++++++++++++---------
t/t2501-cwd-empty.sh | 5 +++--
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/builtin/clean.c b/builtin/clean.c
index 98a2860409b..3ff02bbbffe 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -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;
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index e4502d24d57..b1182390ba3 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 07/11] rebase: do not attempt to remove startup_info->original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (5 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 06/11] clean: do not attempt to remove startup_info->original_cwd Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 08/11] stash: " Elijah Newren via GitGitGadget
` (3 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
sequencer.c | 2 ++
t/t2501-cwd-empty.sh | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index ea96837cde3..83f257e7fa4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index b1182390ba3..52335a8afe9 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 08/11] stash: do not attempt to remove startup_info->original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (6 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 07/11] rebase: " Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
` (2 subsequent siblings)
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/stash.c | 4 +++-
t/t2501-cwd-empty.sh | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/builtin/stash.c b/builtin/stash.c
index a0ccc8654df..de0e432a4ff 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
struct 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)) {
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index 52335a8afe9..be9ef903bd4 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 09/11] dir: avoid incidentally removing the original_cwd in remove_path()
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (7 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 08/11] stash: " Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
dir.c | 3 +++
dir.h | 6 +++++-
t/t2501-cwd-empty.sh | 12 ++++--------
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/dir.c b/dir.c
index 94489298f4c..97d6b71c872 100644
--- a/dir.c
+++ b/dir.c
@@ -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);
}
diff --git a/dir.h b/dir.h
index 83f46c0fb4c..d6a5d03bec2 100644
--- a/dir.h
+++ b/dir.h
@@ -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);
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index be9ef903bd4..ce2efb9d30a 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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' '
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (8 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 09/11] dir: avoid incidentally removing the original_cwd in remove_path() Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
2021-12-09 5:08 ` [PATCH v6 11/11] t2501: simplify the tests since we can now assume desired behavior Elijah Newren via GitGitGadget
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
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.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
builtin/rm.c | 3 ++-
dir.c | 12 +++++++++---
dir.h | 3 +++
t/t2501-cwd-empty.sh | 5 -----
4 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/builtin/rm.c b/builtin/rm.c
index 3d0967cdc11..b4132e5d8ee 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -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;
diff --git a/dir.c b/dir.c
index 97d6b71c872..52064345a6b 100644
--- a/dir.c
+++ b/dir.c
@@ -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.
diff --git a/dir.h b/dir.h
index d6a5d03bec2..8e02dfb505d 100644
--- a/dir.h
+++ b/dir.h
@@ -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.
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index ce2efb9d30a..bc92230f2f2 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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" &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* [PATCH v6 11/11] t2501: simplify the tests since we can now assume desired behavior
2021-12-09 5:08 ` [PATCH v6 " Elijah Newren via GitGitGadget
` (9 preceding siblings ...)
2021-12-09 5:08 ` [PATCH v6 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd Elijah Newren via GitGitGadget
@ 2021-12-09 5:08 ` Elijah Newren via GitGitGadget
10 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren via GitGitGadget @ 2021-12-09 5:08 UTC (permalink / raw)
To: git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley, Derrick Stolee, Eric Sunshine, Phillip Wood,
Elijah Newren, Elijah Newren
From: Elijah Newren <newren@gmail.com>
We no longer are dealing with a mixture of previous and desired
behavior, so simplify the tests a bit.
Acked-by: Derrick Stolee <stolee@gmail.com>
Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------
1 file changed, 36 insertions(+), 87 deletions(-)
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index bc92230f2f2..f6d8d7d03d7 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -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
'
--
gitgitgadget
^ permalink raw reply related [flat|nested] 163+ messages in thread
* Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-26 22:40 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Elijah Newren via GitGitGadget
` (12 preceding siblings ...)
2021-11-29 22:37 ` [PATCH v4 " Elijah Newren via GitGitGadget
@ 2021-11-30 11:04 ` Phillip Wood
2021-12-01 0:03 ` Elijah Newren
13 siblings, 1 reply; 163+ messages in thread
From: Phillip Wood @ 2021-11-30 11:04 UTC (permalink / raw)
To: Elijah Newren via GitGitGadget, git
Cc: Jeff King, René Scharfe,
Ævar Arnfjörð Bjarmason, Elijah Newren, Glen Choo,
Philip Oakley
Hi Elijah
On 26/11/2021 22:40, Elijah Newren via GitGitGadget wrote:
> 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_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/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/
> [2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/
> [3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]
> https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/
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
> 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; 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 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 | 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.sh
>
>
> base-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/1140
>
> Range-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 <newren@gmail.com>
>
> @@ 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 <newren@gmail.com>
>
> ## 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 <newren@gmail.com>
>
> ## 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 <newren@gmail.com>
>
> ## 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
>
^ permalink raw reply [flat|nested] 163+ messages in thread
* Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty
2021-11-30 11:04 ` [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty Phillip Wood
@ 2021-12-01 0:03 ` Elijah Newren
0 siblings, 0 replies; 163+ messages in thread
From: Elijah Newren @ 2021-12-01 0:03 UTC (permalink / raw)
To: Phillip Wood
Cc: Elijah Newren via GitGitGadget, Git Mailing List, Jeff King,
René Scharfe, Ævar Arnfjörð Bjarmason,
Glen Choo, Philip Oakley
Hi Phillip,
On Tue, Nov 30, 2021 at 3:04 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Elijah
>
> On 26/11/2021 22:40, Elijah Newren via GitGitGadget wrote:
> > 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_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/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/
> > [2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/
> > [3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]
> > https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/
>
>
> 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.)
^ permalink raw reply [flat|nested] 163+ messages in thread