{"thread":{"id":"56952","subject":"[PATCH 0/8] Avoid removing the current working directory, even if it becomes empty","startedAt":"2021-11-21T00:47:05Z","lastAt":"2022-03-11T12:01:52Z","messageCount":163,"participants":["Elijah Newren via GitGitGadget","Junio C Hamano","René Scharfe","Ævar Arnfjörð Bjarmason","Elijah Newren","Glen Choo","Philip Oakley","Derrick Stolee","Eric Sunshine","Phillip Wood"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"441867","messageId":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":null,"subject":"[PATCH 0/8] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:52Z","receivedAt":"2021-11-21T00:47:05Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). We already refuse to remove\ndirectories that have untracked files within them[1], preferring to show an\nerror; with this series, we tweak that rule a bit to also refuse to remove\nthe current working directory even if it has no untracked files within it.\n\nPeff endorsed the idea behind this series at [2] and even pointed out a\ncorner case that he suggested (and I agree) would probably be more\nproblematic to attempt to address than to leave be.\n\nThis series has a minor textual conflict with so/stash-staged (in next),\nbecause both series add a new parameter to the do_push_stash() function. The\ncorrect resolution is simple -- just to take both new parameters. However,\nlet me know if you'd rather I rebased onto so/stash-staged.\n\n(Sorry for the delay; I know I promised this months ago in response to [3],\nbut it was blocked by some preliminary series and then by my absence during\na migration at $DAYJOB).\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3]\nhttps://lore.kernel.org/git/c0557284-8f82-76cc-8c47-0b1bc9f2ce5a@rawbw.com/\n\nElijah Newren (8):\n  t2501: add various tests for removing the current working directory\n  repository, setup: introduce the_cwd\n  unpack-trees: refuse to remove the current working directory\n  unpack-trees: add special cwd handling\n  symlinks: do not include current working directory in dir removal\n  clean: do not attempt to remove current working directory\n  stash: do not attempt to remove current working directory\n  dir: avoid removing the current working directory\n\n builtin/clean.c      |  29 +++--\n builtin/stash.c      |  13 ++-\n dir.c                |  11 +-\n repository.c         |   1 +\n repository.h         |   1 +\n setup.c              |   2 +\n symlinks.c           |  12 +-\n t/t2501-cwd-empty.sh | 255 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  28 ++++-\n unpack-trees.h       |   1 +\n 10 files changed, 329 insertions(+), 24 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v1\nPull-Request: https://github.com/git/git/pull/1140\n-- \ngitgitgadget\n"},{"id":"441868","messageId":"7b0c665fb75d3d73d9d8d03b629a09a0ec4244e6.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 2/8] repository, setup: introduce the_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:54Z","receivedAt":"2021-11-21T00:47:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands (and likely a number of non-git commands) run from that\ndirectory to get confused and fail with a message about being unable to\nread the current working directory.  That confuses end users,\nparticularly since the command they get the error from is not the one\nthat caused the problem; the problem came from the side-effect of some\nprevious command.\n\nWe would like to avoid removing the current working directory; towards\nthis end, introduce a new the_cwd variable that tracks the current\nworking directory.  Subsequent commits will make use of this new\nvariable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n repository.c | 1 +\n repository.h | 1 +\n setup.c      | 2 ++\n 3 files changed, 4 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex c5b90ba93ea..69a106c553c 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -17,6 +17,7 @@\n static struct repository the_repo;\n struct repository *the_repository;\n struct index_state the_index;\n+char *the_cwd;\n \n void initialize_the_repository(void)\n {\ndiff --git a/repository.h b/repository.h\nindex a057653981c..45de85d18ef 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -147,6 +147,7 @@ struct repository {\n };\n \n extern struct repository *the_repository;\n+extern char *the_cwd;\n \n /*\n  * Define a custom repository layout. Any field can be NULL, which\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..4466fa55af3 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -887,6 +887,7 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,\n \t\tset_git_dir(gitdirenv, 1);\n \t\tif (chdir(worktree))\n \t\t\tdie_errno(_(\"cannot chdir to '%s'\"), worktree);\n+\t\tthe_cwd = xstrdup(cwd->buf + offset);\n \t\tstrbuf_addch(cwd, '/');\n \t\tfree(gitfile);\n \t\treturn cwd->buf + offset;\n@@ -940,6 +941,7 @@ static const char *setup_discovered_git_dir(const char *gitdir,\n \t/* Make \"offset\" point past the '/' (already the case for root dirs) */\n \tif (offset != offset_1st_component(cwd->buf))\n \t\toffset++;\n+\tthe_cwd = xstrdup(cwd->buf + offset);\n \t/* Add a '/' at the end */\n \tstrbuf_addch(cwd, '/');\n \treturn cwd->buf + offset;\n-- \ngitgitgadget\n\n"},{"id":"441869","messageId":"0b71996a3b462d4147fb792b20057544b9ef1710.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:53Z","receivedAt":"2021-11-21T00:47:13Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove empty working directories, especially if\nthey are in the way of placing needed files.  That is normally fine, but\nremoving the current working directory can cause confusion for the user\nwhen they run subsequent commands.  Add some tests checking for such\nproblems.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 255 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 255 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..5dfb456a691\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,255 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\tmkdir subdir &&\n+\ttest_commit subdir/file &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tgit rm subdir/file.t &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile\n+'\n+\n+test_expect_failure 'checkout does not clean cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit checkout init &&\n+\t\tcd ..\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'checkout fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'reset --hard does not clean cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit reset --hard init &&\n+\t\tcd ..\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\ttest_must_fail git reset --hard fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove.*the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'merge does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit merge fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'merge fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git merge fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit cherry-pick fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git cherry-pick fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'rebase does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit rebase foo/bar/baz fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'rebase fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git rebase foo/bar/baz fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'revert does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit revert subdir/file\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'revert fails if cwd needs to be removed' '\n+\tgit checkout fd_conflict &&\n+\tgit revert HEAD &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git revert HEAD 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'rm does not remove cwd incidentally' '\n+\ttest_when_finished \"git reset --hard\" &&\n+\tgit checkout foo/bar/baz &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit rm bar/baz.t\n+\t) &&\n+\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'apply does not remove cwd incidentally' '\n+\ttest_when_finished \"git reset --hard\" &&\n+\tgit checkout foo/bar/baz &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit diff subdir/file init | git apply\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'clean does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir empty &&\n+\tmkdir untracked &&\n+\t>untracked/random &&\n+\t(\n+\t\tcd untracked &&\n+\t\tgit clean -fd -e warnings :/ >../warnings &&\n+\t\tgrep \"Refusing to remove current working directory\" ../warnings\n+\t) &&\n+\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\ttest_path_is_dir untracked\n+'\n+\n+test_expect_failure 'stash does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir untracked &&\n+\t>untracked/random &&\n+\t(\n+\t\tcd untracked &&\n+\t\tgit stash --include-untracked &&\n+\t\tgit status\n+\t) &&\n+\n+\ttest_path_is_missing untracked/random &&\n+\ttest_path_is_dir untracked\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"441870","messageId":"39830fffd45e103b608e2ac7869a509fcd5dc7db.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 4/8] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:56Z","receivedAt":"2021-11-21T00:47:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the current\nworking directory being in the way.  But in the\nUNPACK_RESET_OVERWRITE_UNTRACKED case, there is no untracked checking to\nbe done, so we simply add a special case near the top of\nverify_absent_1.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 12 ++++++++++--\n 2 files changed, 11 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 212676d71c3..526d8ec2ee3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -67,7 +67,7 @@ test_expect_failure 'reset --hard does not clean cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 8879b8d6c8c..d1ce4f4a35b 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2260,10 +2260,18 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking cwd... */\n+\t\tif (the_cwd && !strcmp(the_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"441871","messageId":"94bf468be0972ce3d6037e6688749cab01269b14.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 3/8] unpack-trees: refuse to remove the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:55Z","receivedAt":"2021-11-21T00:47:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 10 +++++-----\n unpack-trees.c       | 16 ++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 18 insertions(+), 9 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 5dfb456a691..212676d71c3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -38,7 +38,7 @@ test_expect_failure 'checkout does not clean cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'checkout fails if cwd needs to be removed' '\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -95,7 +95,7 @@ test_expect_failure 'merge does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'merge fails if cwd needs to be removed' '\n+test_expect_success 'merge fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -122,7 +122,7 @@ test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -149,7 +149,7 @@ test_expect_failure 'rebase does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'rebase fails if cwd needs to be removed' '\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -176,7 +176,7 @@ test_expect_failure 'revert does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'revert fails if cwd needs to be removed' '\n+test_expect_success 'revert fails if cwd needs to be removed' '\n \tgit checkout fd_conflict &&\n \tgit revert HEAD &&\n \ttest_when_finished \"git clean -fdx\" &&\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..8879b8d6c8c 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,11 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose the current working directory. */\n+\tif (the_cwd && !strcmp(the_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"441872","messageId":"93005b17c2880407872340d4927007db0d9e97b6.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 7/8] stash: do not attempt to remove current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:59Z","receivedAt":"2021-11-21T00:47:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 13 ++++++++-----\n t/t2501-cwd-empty.sh |  2 +-\n 2 files changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..556287cdb56 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1404,7 +1404,8 @@ static int create_stash(int argc, const char **argv, const char *prefix)\n \treturn ret;\n }\n \n-static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,\n+static int do_push_stash(const struct pathspec *ps, const char *stash_msg,\n+\t\t\t const char *prefix, int quiet,\n \t\t\t int keep_index, int patch_mode, int include_untracked)\n {\n \tint ret = 0;\n@@ -1485,8 +1486,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (prefix)\n+\t\t\t\tstrvec_pushl(&cp.args, \"-C\", prefix, NULL);\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\n@@ -1656,8 +1659,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"--pathspec-file-nul requires --pathspec-from-file\"));\n \t}\n \n-\treturn do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,\n-\t\t\t     include_untracked);\n+\treturn do_push_stash(&ps, stash_msg, prefix, quiet, keep_index,\n+\t\t\t     patch_mode, include_untracked);\n }\n \n static int save_stash(int argc, const char **argv, const char *prefix)\n@@ -1693,7 +1696,7 @@ static int save_stash(int argc, const char **argv, const char *prefix)\n \t\tstash_msg = strbuf_join_argv(&stash_msg_buf, argc, argv, ' ');\n \n \tmemset(&ps, 0, sizeof(ps));\n-\tret = do_push_stash(&ps, stash_msg, quiet, keep_index,\n+\tret = do_push_stash(&ps, stash_msg, prefix, quiet, keep_index,\n \t\t\t    patch_mode, include_untracked);\n \n \tstrbuf_release(&stash_msg_buf);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 20e1b6adede..ff4e7cd89fa 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n \ttest_path_is_dir untracked\n '\n \n-test_expect_failure 'stash does not remove cwd incidentally' '\n+test_expect_success 'stash does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"441873","messageId":"69bcaf0aab28fec145742f7183b1d89b12eaf0f9.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 6/8] clean: do not attempt to remove current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:58Z","receivedAt":"2021-11-21T00:47:20Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 29 ++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  2 +-\n 2 files changed, 21 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..17d54be57b7 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -231,16 +233,25 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\tint prefixlen = prefix ? strlen(prefix) : 0;\n+\t\tif (prefix &&\n+\t\t    path->len == prefixlen + 2 &&\n+\t\t    !strncmp(path->buf, prefix, prefixlen) &&\n+\t\t    !strcmp(path->buf + prefixlen, \"./\")) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex b92e1a9bb16..20e1b6adede 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -218,7 +218,7 @@ test_expect_failure 'apply does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'clean does not remove cwd incidentally' '\n+test_expect_success 'clean does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"441874","messageId":"8a69d2878c99e1b9321e57073d266cf797dc5630.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 5/8] symlinks: do not include current working directory in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:46:57Z","receivedAt":"2021-11-21T00:47:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude the current working directory and leave it around so\nthat subsequent git commands (and non-git commands) that the user runs\nafterwards don't cause the user to get confused.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           | 12 +++++++++++-\n t/t2501-cwd-empty.sh | 12 ++++++------\n 2 files changed, 17 insertions(+), 7 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..84622bedcde 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -275,11 +275,18 @@ static int threaded_has_dirs_only_path(struct cache_def *cache, const char *name\n \n static struct strbuf removal = STRBUF_INIT;\n \n+static int cant_remove(char *dirname)\n+{\n+\tif (the_cwd && !strcmp(dirname, the_cwd))\n+\t\treturn 1;\n+\treturn rmdir(dirname);\n+}\n+\n static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif (cant_remove(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +300,9 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (the_cwd && !strcmp(name, the_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 526d8ec2ee3..b92e1a9bb16 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -24,7 +24,7 @@ test_expect_success setup '\n \tgit commit -m dirORfile\n '\n \n-test_expect_failure 'checkout does not clean cwd incidentally' '\n+test_expect_success 'checkout does not clean cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -53,7 +53,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'reset --hard does not clean cwd incidentally' '\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -82,7 +82,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'merge does not remove cwd incidentally' '\n+test_expect_success 'merge does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -109,7 +109,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n+test_expect_success 'cherry-pick does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -136,7 +136,7 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'rebase does not remove cwd incidentally' '\n+test_expect_success 'rebase does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -163,7 +163,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'revert does not remove cwd incidentally' '\n+test_expect_success 'revert does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"441875","messageId":"a5528cbb14ddbbf26cde873e3f3e95744d59b950.1637455620.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-21T00:47:00Z","receivedAt":"2021-11-21T00:47:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\ndir has a convenient remove_path() helper that will both remove a file\nin a directory and remove its containing directory if it becomes empty\nas a result of the removal, recursing all the way up.  However, we do\nnot want the current working directory to be removed, even if it becomes\nempty.\n\ndir also has a remove_dir_recursively() function which appears to mostly\nbe used to remove metadata directories or temporary directories or\nsubmodules or worktrees.  I am not sure if it needs to be protected\nagainst removing the current working directory, but did so for good\nmeasure.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                | 11 ++++++++---\n t/t2501-cwd-empty.sh |  4 ++--\n 2 files changed, 10 insertions(+), 5 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..15e7639a158 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3259,9 +3259,12 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (the_cwd && !strcmp(the_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\n@@ -3327,6 +3330,8 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (the_cwd && !strcmp(the_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex ff4e7cd89fa..4362e7b15e5 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -191,7 +191,7 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'rm does not remove cwd incidentally' '\n+test_expect_success 'rm does not remove cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n@@ -205,7 +205,7 @@ test_expect_failure 'rm does not remove cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'apply does not remove cwd incidentally' '\n+test_expect_success 'apply does not remove cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n-- \ngitgitgadget\n"},{"id":"441881","messageId":"xmqqv90mlyzt.fsf@gitster.g","threadId":"56952","inReplyTo":"7b0c665fb75d3d73d9d8d03b629a09a0ec4244e6.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/8] repository, setup: introduce the_cwd","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-21T08:00:22Z","receivedAt":"2021-11-21T08:00:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Removing the current working directory causes all subsequent git\n> commands (and likely a number of non-git commands) run from that\n> directory to get confused and fail with a message about being unable to\n> read the current working directory.  That confuses end users,\n> particularly since the command they get the error from is not the one\n> that caused the problem; the problem came from the side-effect of some\n> previous command.\n>\n> We would like to avoid removing the current working directory; towards\n> this end, introduce a new the_cwd variable that tracks the current\n> working directory.  Subsequent commits will make use of this new\n> variable.\n\nMaybe a stupid question, but how is this different from doing getcwd()\nand storing it away to the_cwd, or adding a check to see if the\ndirectory we are about to rmdir() is the cwd, next to the existing\ncheck that we do to see if that directory has some untracked files?\n\nI am wondering how we are going to make sure that the_cwd is always\nset to, and maintained to be, the correct value, even in the future\nwhen these code paths change.  I also wonder if it might be safer to\nlearn what the value of cwd is very near the place where it will\nbecome needed (i.e. the callsites of such rmdir() of a directory\ninside working tree), instead of caching.\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  repository.c | 1 +\n>  repository.h | 1 +\n>  setup.c      | 2 ++\n>  3 files changed, 4 insertions(+)\n>\n> diff --git a/repository.c b/repository.c\n> index c5b90ba93ea..69a106c553c 100644\n> --- a/repository.c\n> +++ b/repository.c\n> @@ -17,6 +17,7 @@\n>  static struct repository the_repo;\n>  struct repository *the_repository;\n>  struct index_state the_index;\n> +char *the_cwd;\n>  \n>  void initialize_the_repository(void)\n>  {\n> diff --git a/repository.h b/repository.h\n> index a057653981c..45de85d18ef 100644\n> --- a/repository.h\n> +++ b/repository.h\n> @@ -147,6 +147,7 @@ struct repository {\n>  };\n>  \n>  extern struct repository *the_repository;\n> +extern char *the_cwd;\n>  \n>  /*\n>   * Define a custom repository layout. Any field can be NULL, which\n> diff --git a/setup.c b/setup.c\n> index 347d7181ae9..4466fa55af3 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -887,6 +887,7 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,\n>  \t\tset_git_dir(gitdirenv, 1);\n>  \t\tif (chdir(worktree))\n>  \t\t\tdie_errno(_(\"cannot chdir to '%s'\"), worktree);\n> +\t\tthe_cwd = xstrdup(cwd->buf + offset);\n>  \t\tstrbuf_addch(cwd, '/');\n>  \t\tfree(gitfile);\n>  \t\treturn cwd->buf + offset;\n> @@ -940,6 +941,7 @@ static const char *setup_discovered_git_dir(const char *gitdir,\n>  \t/* Make \"offset\" point past the '/' (already the case for root dirs) */\n>  \tif (offset != offset_1st_component(cwd->buf))\n>  \t\toffset++;\n> +\tthe_cwd = xstrdup(cwd->buf + offset);\n>  \t/* Add a '/' at the end */\n>  \tstrbuf_addch(cwd, '/');\n>  \treturn cwd->buf + offset;\n"},{"id":"441882","messageId":"xmqqo86elyht.fsf@gitster.g","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/8] Avoid removing the current working directory, even if it becomes empty","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-21T08:11:10Z","receivedAt":"2021-11-21T08:11:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Traditionally, if folks run git commands such as checkout or rebase from a\n> subdirectory, that git command could remove their current working directory\n> and result in subsequent git and non-git commands either getting confused or\n> printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> working directory: No such file or directory\"). We already refuse to remove\n> directories that have untracked files within them[1], preferring to show an\n> error; with this series, we tweak that rule a bit to also refuse to remove\n> the current working directory even if it has no untracked files within it.\n\nThe goal is roughly that we do not allow rmdir() of a directory at\n\"prefix\" in the working tree, or any parent directory of it, for\nfunctions that uses RUN_SETUP?  By the time we attempt to rmdir(),\nmost likely we'd chdir()'ed up to the top of the working tree\nourselves, so we need to remember where the original cwd was when we\nstarted, and protecting the original cwd would mean that we protect\nthe directory in which the process that spawned us, like the user's\ninteractive shell, is still sitting, which makes sense.\n\n\n"},{"id":"441883","messageId":"776abd16-53d5-46c0-8008-c518a4415f7d@web.de","threadId":"56952","inReplyTo":"7b0c665fb75d3d73d9d8d03b629a09a0ec4244e6.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/8] repository, setup: introduce the_cwd","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2021-11-21T08:56:33Z","receivedAt":"2021-11-21T08:56:51Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 21.11.21 um 01:46 schrieb Elijah Newren via GitGitGadget:\n> From: Elijah Newren <newren@gmail.com>\n>\n> Removing the current working directory causes all subsequent git\n> commands (and likely a number of non-git commands) run from that\n> directory to get confused and fail with a message about being unable to\n> read the current working directory.  That confuses end users,\n> particularly since the command they get the error from is not the one\n> that caused the problem; the problem came from the side-effect of some\n> previous command.\n>\n> We would like to avoid removing the current working directory;\n\nA worthy goal.\n\n> towards\n> this end, introduce a new the_cwd variable that tracks the current\n> working directory.  Subsequent commits will make use of this new\n> variable.\n\nWhy make it a global variable instead of getting the working directory\nin the places that try to delete directories?  (Honest question, not a\nsuggestion.)\n\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  repository.c | 1 +\n>  repository.h | 1 +\n>  setup.c      | 2 ++\n>  3 files changed, 4 insertions(+)\n>\n> diff --git a/repository.c b/repository.c\n> index c5b90ba93ea..69a106c553c 100644\n> --- a/repository.c\n> +++ b/repository.c\n> @@ -17,6 +17,7 @@\n>  static struct repository the_repo;\n>  struct repository *the_repository;\n>  struct index_state the_index;\n> +char *the_cwd;\n>\n>  void initialize_the_repository(void)\n>  {\n> diff --git a/repository.h b/repository.h\n> index a057653981c..45de85d18ef 100644\n> --- a/repository.h\n> +++ b/repository.h\n> @@ -147,6 +147,7 @@ struct repository {\n>  };\n>\n>  extern struct repository *the_repository;\n> +extern char *the_cwd;\n>\n>  /*\n>   * Define a custom repository layout. Any field can be NULL, which\n> diff --git a/setup.c b/setup.c\n> index 347d7181ae9..4466fa55af3 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -887,6 +887,7 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,\n>  \t\tset_git_dir(gitdirenv, 1);\n>  \t\tif (chdir(worktree))\n>  \t\t\tdie_errno(_(\"cannot chdir to '%s'\"), worktree);\n> +\t\tthe_cwd = xstrdup(cwd->buf + offset);\n>  \t\tstrbuf_addch(cwd, '/');\n>  \t\tfree(gitfile);\n>  \t\treturn cwd->buf + offset;\n> @@ -940,6 +941,7 @@ static const char *setup_discovered_git_dir(const char *gitdir,\n>  \t/* Make \"offset\" point past the '/' (already the case for root dirs) */\n>  \tif (offset != offset_1st_component(cwd->buf))\n>  \t\toffset++;\n> +\tthe_cwd = xstrdup(cwd->buf + offset);\n>  \t/* Add a '/' at the end */\n>  \tstrbuf_addch(cwd, '/');\n>  \treturn cwd->buf + offset;\n>\n\n"},{"id":"441884","messageId":"150f9c88-f175-963d-2947-7ae9f3a2ef12@web.de","threadId":"56952","inReplyTo":"8a69d2878c99e1b9321e57073d266cf797dc5630.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 5/8] symlinks: do not include current working directory in dir removal","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2021-11-21T08:56:36Z","receivedAt":"2021-11-21T08:56:51Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 21.11.21 um 01:46 schrieb Elijah Newren via GitGitGadget:\n> From: Elijah Newren <newren@gmail.com>\n>\n> symlinks has a pair of schedule_dir_for_removal() and\n> remove_scheduled_dirs() functions that ensure that directories made\n> empty by removing other files also themselves get removed.  However, we\n> want to exclude the current working directory and leave it around so\n> that subsequent git commands (and non-git commands) that the user runs\n> afterwards don't cause the user to get confused.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  symlinks.c           | 12 +++++++++++-\n>  t/t2501-cwd-empty.sh | 12 ++++++------\n>  2 files changed, 17 insertions(+), 7 deletions(-)\n>\n> diff --git a/symlinks.c b/symlinks.c\n> index 5232d02020c..84622bedcde 100644\n> --- a/symlinks.c\n> +++ b/symlinks.c\n> @@ -275,11 +275,18 @@ static int threaded_has_dirs_only_path(struct cache_def *cache, const char *name\n>\n>  static struct strbuf removal = STRBUF_INIT;\n>\n> +static int cant_remove(char *dirname)\n> +{\n> +\tif (the_cwd && !strcmp(dirname, the_cwd))\n\nInitializing the_cwd to an empty string would allow removing the NULL check\neverywhere.\n\nIs strcmp() sufficient or do we need fspathcmp() in these kinds of checks?\nDo we need to worry about normalizing directory separators?\n\n> +\t\treturn 1;\n> +\treturn rmdir(dirname);\n> +}\n\nI wouldn't expect a function of that name to actually try to remove\nthe directory.  Or with that body to require a non-const dirname.\nIt's used only once, perhaps inline it?\n\n> +\n>  static void do_remove_scheduled_dirs(int new_len)\n>  {\n>  \twhile (removal.len > new_len) {\n>  \t\tremoval.buf[removal.len] = '\\0';\n> -\t\tif (rmdir(removal.buf))\n> +\t\tif (cant_remove(removal.buf))\n>  \t\t\tbreak;\n>  \t\tdo {\n>  \t\t\tremoval.len--;\n> @@ -293,6 +300,9 @@ void schedule_dir_for_removal(const char *name, int len)\n>  {\n>  \tint match_len, last_slash, i, previous_slash;\n>\n> +\tif (the_cwd && !strcmp(name, the_cwd))\n> +\t\treturn;\t/* Do not remove the current working directory */\n> +\n>  \tmatch_len = last_slash = i =\n>  \t\tlongest_path_match(name, len, removal.buf, removal.len,\n>  \t\t\t\t   &previous_slash);\n"},{"id":"441886","messageId":"211121.86bl2d1jo3.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"69bcaf0aab28fec145742f7183b1d89b12eaf0f9.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 6/8] clean: do not attempt to remove current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-21T17:51:12Z","receivedAt":"2021-11-21T17:51:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n\n> +\t\tint prefixlen = prefix ? strlen(prefix) : 0;\n\nnit: s/int/size_t/\n"},{"id":"441887","messageId":"211121.867dd11jbl.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"0b71996a3b462d4147fb792b20057544b9ef1710.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-21T17:57:11Z","receivedAt":"2021-11-21T17:59:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n\n> +test_expect_failure 'checkout fails if cwd needs to be removed' '\n> +\tgit checkout foo/bar/baz &&\n> +\ttest_when_finished \"git clean -fdx\" &&\n> +\n> +\tmkdir dirORfile &&\n> +\t(\n> +\t\tcd dirORfile &&\n> +\n> +\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n> +\t\tgrep \"Refusing to remove the current working directory\" ../error\n> +\t) &&\n> +\n> +\ttest_path_is_dir dirORfile\n\n\nI'd find this & the rest of this series much easier to understand if we\nstarted out by positively asserting the current behavior here, and\ndidn't test_cmp/grep for erro r messages that don't exist anymore.\n\nIt would also help to show how exactly operations that currently \"work\"\nbehave, e.g. if you git checkout a revision within \"t/\" which is a\nsubdir, and it turns into \"t\" the file etc.\n\nI'm also generally knee-jerk reactionary to test_expect_failure for its\nfragility[1], but in this case more because it makes explaining this\nstep-by-step harder & not as obvious.\n\n1. https://lore.kernel.org/git/87tuhmk19c.fsf@evledraar.gmail.com/\n"},{"id":"442049","messageId":"CABPp-BHaGjoX3V6yfFLS=i0bH8AiN6Kpej43yPqgkbtF3FKNRQ@mail.gmail.com","threadId":"56952","inReplyTo":"xmqqv90mlyzt.fsf@gitster.g","subject":"Re: [PATCH 2/8] repository, setup: introduce the_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-22T22:38:00Z","receivedAt":"2021-11-22T22:38:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Nov 21, 2021 at 12:00 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Removing the current working directory causes all subsequent git\n> > commands (and likely a number of non-git commands) run from that\n> > directory to get confused and fail with a message about being unable to\n> > read the current working directory.  That confuses end users,\n> > particularly since the command they get the error from is not the one\n> > that caused the problem; the problem came from the side-effect of some\n> > previous command.\n> >\n> > We would like to avoid removing the current working directory; towards\n> > this end, introduce a new the_cwd variable that tracks the current\n> > working directory.  Subsequent commits will make use of this new\n> > variable.\n>\n> Maybe a stupid question, but how is this different from doing getcwd()\n> and storing it away to the_cwd\n\nThe exact output of getcwd() is an absolute path, whereas it's easier\nto perform later checks if we have a path that is relative to the\ntoplevel working directory.  Also, setup.c already calls getcwd() (via\nstrbuf_getcwd()) and then massages it, and I'd have to do the exact\nsame type of massaging.  So, instead of calling getcwd() again and\nre-massaging its output into the format I want, I just reuse the\nexisting call of getcwd() and its massaged output.\n\n> or adding a check to see if the\n> directory we are about to rmdir() is the cwd, next to the existing\n> check that we do to see if that directory has some untracked files?\n>\n> I am wondering how we are going to make sure that the_cwd is always\n> set to, and maintained to be, the correct value, even in the future\n> when these code paths change.  I also wonder if it might be safer to\n> learn what the value of cwd is very near the place where it will\n> become needed (i.e. the callsites of such rmdir() of a directory\n> inside working tree), instead of caching.\n\nI need to clean up my wording a bit, to clarify this.  Doing as you\nsuggest here would protect the wrong thing.  git has long done an\nautomatic chdir() at startup to the toplevel working tree, whereas\nwhat we want to protect is the original current working directory as\nof the time git started.  The original current working directory is\nlikely also still the current working directory of the parent process\nthat spawned us, and if that parent process is a shell, any subsequent\ncommands executed from there can be somewhat confusing when its\ncurrent working directory is removed.\n\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  repository.c | 1 +\n> >  repository.h | 1 +\n> >  setup.c      | 2 ++\n> >  3 files changed, 4 insertions(+)\n> >\n> > diff --git a/repository.c b/repository.c\n> > index c5b90ba93ea..69a106c553c 100644\n> > --- a/repository.c\n> > +++ b/repository.c\n> > @@ -17,6 +17,7 @@\n> >  static struct repository the_repo;\n> >  struct repository *the_repository;\n> >  struct index_state the_index;\n> > +char *the_cwd;\n> >\n> >  void initialize_the_repository(void)\n> >  {\n> > diff --git a/repository.h b/repository.h\n> > index a057653981c..45de85d18ef 100644\n> > --- a/repository.h\n> > +++ b/repository.h\n> > @@ -147,6 +147,7 @@ struct repository {\n> >  };\n> >\n> >  extern struct repository *the_repository;\n> > +extern char *the_cwd;\n> >\n> >  /*\n> >   * Define a custom repository layout. Any field can be NULL, which\n> > diff --git a/setup.c b/setup.c\n> > index 347d7181ae9..4466fa55af3 100644\n> > --- a/setup.c\n> > +++ b/setup.c\n> > @@ -887,6 +887,7 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,\n> >               set_git_dir(gitdirenv, 1);\n> >               if (chdir(worktree))\n> >                       die_errno(_(\"cannot chdir to '%s'\"), worktree);\n> > +             the_cwd = xstrdup(cwd->buf + offset);\n> >               strbuf_addch(cwd, '/');\n> >               free(gitfile);\n> >               return cwd->buf + offset;\n> > @@ -940,6 +941,7 @@ static const char *setup_discovered_git_dir(const char *gitdir,\n> >       /* Make \"offset\" point past the '/' (already the case for root dirs) */\n> >       if (offset != offset_1st_component(cwd->buf))\n> >               offset++;\n> > +     the_cwd = xstrdup(cwd->buf + offset);\n> >       /* Add a '/' at the end */\n> >       strbuf_addch(cwd, '/');\n> >       return cwd->buf + offset;\n"},{"id":"442062","messageId":"CABPp-BH2C1J9AAYnxcptoqgE5WeUCV=4W7qdW0P9KyMi2L3Abw@mail.gmail.com","threadId":"56952","inReplyTo":"776abd16-53d5-46c0-8008-c518a4415f7d@web.de","subject":"Re: [PATCH 2/8] repository, setup: introduce the_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-22T23:09:22Z","receivedAt":"2021-11-22T23:09:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Nov 21, 2021 at 12:56 AM René Scharfe <l.s.r@web.de> wrote:\n>\n> Am 21.11.21 um 01:46 schrieb Elijah Newren via GitGitGadget:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Removing the current working directory causes all subsequent git\n> > commands (and likely a number of non-git commands) run from that\n> > directory to get confused and fail with a message about being unable to\n> > read the current working directory.  That confuses end users,\n> > particularly since the command they get the error from is not the one\n> > that caused the problem; the problem came from the side-effect of some\n> > previous command.\n> >\n> > We would like to avoid removing the current working directory;\n>\n> A worthy goal.\n>\n> > towards\n> > this end, introduce a new the_cwd variable that tracks the current\n> > working directory.  Subsequent commits will make use of this new\n> > variable.\n>\n> Why make it a global variable instead of getting the working directory\n> in the places that try to delete directories?  (Honest question, not a\n> suggestion.)\n\nAs I mentioned in my response to Junio, I need to be clearer that what\nI want to protect is the current working directory as of the startup\nof the git process, as a proxy for the current working directory of\nthe parent process, so that subsequent commands started from the\nparent process don't get confused.\n\nAs such, we don't want to get the working directory again later,\nbecause that'd give us the wrong thing.  Also, setup.c has some nice\nmassaging of getcwd() from an absolute path into a relative path that\nis much more convenient for us to do our later comparisons, and we'd\nrather not do those additional tweaks with every check.  Since the\nrelative path from the project root to the _original_ current working\ndirectory is a single value, a global seemed to make sense for saving\nit.  Perhaps I should tweak it to be const as well.\n\n>\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  repository.c | 1 +\n> >  repository.h | 1 +\n> >  setup.c      | 2 ++\n> >  3 files changed, 4 insertions(+)\n> >\n> > diff --git a/repository.c b/repository.c\n> > index c5b90ba93ea..69a106c553c 100644\n> > --- a/repository.c\n> > +++ b/repository.c\n> > @@ -17,6 +17,7 @@\n> >  static struct repository the_repo;\n> >  struct repository *the_repository;\n> >  struct index_state the_index;\n> > +char *the_cwd;\n> >\n> >  void initialize_the_repository(void)\n> >  {\n> > diff --git a/repository.h b/repository.h\n> > index a057653981c..45de85d18ef 100644\n> > --- a/repository.h\n> > +++ b/repository.h\n> > @@ -147,6 +147,7 @@ struct repository {\n> >  };\n> >\n> >  extern struct repository *the_repository;\n> > +extern char *the_cwd;\n> >\n> >  /*\n> >   * Define a custom repository layout. Any field can be NULL, which\n> > diff --git a/setup.c b/setup.c\n> > index 347d7181ae9..4466fa55af3 100644\n> > --- a/setup.c\n> > +++ b/setup.c\n> > @@ -887,6 +887,7 @@ static const char *setup_explicit_git_dir(const char *gitdirenv,\n> >               set_git_dir(gitdirenv, 1);\n> >               if (chdir(worktree))\n> >                       die_errno(_(\"cannot chdir to '%s'\"), worktree);\n> > +             the_cwd = xstrdup(cwd->buf + offset);\n> >               strbuf_addch(cwd, '/');\n> >               free(gitfile);\n> >               return cwd->buf + offset;\n> > @@ -940,6 +941,7 @@ static const char *setup_discovered_git_dir(const char *gitdir,\n> >       /* Make \"offset\" point past the '/' (already the case for root dirs) */\n> >       if (offset != offset_1st_component(cwd->buf))\n> >               offset++;\n> > +     the_cwd = xstrdup(cwd->buf + offset);\n> >       /* Add a '/' at the end */\n> >       strbuf_addch(cwd, '/');\n> >       return cwd->buf + offset;\n> >\n>\n"},{"id":"442074","messageId":"CABPp-BF4WtoRig=TUQa76G1sfG3uOiEZw5vk7cGbDtJNahhgnw@mail.gmail.com","threadId":"56952","inReplyTo":"150f9c88-f175-963d-2947-7ae9f3a2ef12@web.de","subject":"Re: [PATCH 5/8] symlinks: do not include current working directory in dir removal","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T00:35:26Z","receivedAt":"2021-11-23T00:35:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Nov 21, 2021 at 12:56 AM René Scharfe <l.s.r@web.de> wrote:\n>\n> Am 21.11.21 um 01:46 schrieb Elijah Newren via GitGitGadget:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > symlinks has a pair of schedule_dir_for_removal() and\n> > remove_scheduled_dirs() functions that ensure that directories made\n> > empty by removing other files also themselves get removed.  However, we\n> > want to exclude the current working directory and leave it around so\n> > that subsequent git commands (and non-git commands) that the user runs\n> > afterwards don't cause the user to get confused.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  symlinks.c           | 12 +++++++++++-\n> >  t/t2501-cwd-empty.sh | 12 ++++++------\n> >  2 files changed, 17 insertions(+), 7 deletions(-)\n> >\n> > diff --git a/symlinks.c b/symlinks.c\n> > index 5232d02020c..84622bedcde 100644\n> > --- a/symlinks.c\n> > +++ b/symlinks.c\n> > @@ -275,11 +275,18 @@ static int threaded_has_dirs_only_path(struct cache_def *cache, const char *name\n> >\n> >  static struct strbuf removal = STRBUF_INIT;\n> >\n> > +static int cant_remove(char *dirname)\n> > +{\n> > +     if (the_cwd && !strcmp(dirname, the_cwd))\n>\n> Initializing the_cwd to an empty string would allow removing the NULL check\n> everywhere.\n\nI actually went and made this change, because although it'd add\nprotection to the toplevel directory (the_cwd is relative to the\ntoplevel), we usually want it protected.  However, dir.c's\nremove_dir_recursively() can be used to remove the toplevel directory\nas well and has an explicit flag for that, so I need to be able to\ndistinguish between uninitialized and explicitly set to the toplevel\ndirectory.\n\n> Is strcmp() sufficient or do we need fspathcmp() in these kinds of checks?\n> Do we need to worry about normalizing directory separators?\n\nGood catch, I should normalize the_cwd when I create it; I was\nessentially creating it to match \"prefix\", but it appears that isn't\npre-emptively normalized, and instead later callers normalize any\ncombination of prefix plus another path.\n\n> > +             return 1;\n> > +     return rmdir(dirname);\n> > +}\n>\n> I wouldn't expect a function of that name to actually try to remove\n> the directory.  Or with that body to require a non-const dirname.\n> It's used only once, perhaps inline it?\n\nSure.\n\n> > +\n> >  static void do_remove_scheduled_dirs(int new_len)\n> >  {\n> >       while (removal.len > new_len) {\n> >               removal.buf[removal.len] = '\\0';\n> > -             if (rmdir(removal.buf))\n> > +             if (cant_remove(removal.buf))\n> >                       break;\n> >               do {\n> >                       removal.len--;\n> > @@ -293,6 +300,9 @@ void schedule_dir_for_removal(const char *name, int len)\n> >  {\n> >       int match_len, last_slash, i, previous_slash;\n> >\n> > +     if (the_cwd && !strcmp(name, the_cwd))\n> > +             return; /* Do not remove the current working directory */\n> > +\n> >       match_len = last_slash = i =\n> >               longest_path_match(name, len, removal.buf, removal.len,\n> >                                  &previous_slash);\n"},{"id":"442075","messageId":"20211123003958.3978-1-chooglen@google.com","threadId":"56952","inReplyTo":"a5528cbb14ddbbf26cde873e3f3e95744d59b950.1637455620.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2021-11-23T00:39:58Z","receivedAt":"2021-11-23T00:40:03Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"> @@ -3259,9 +3259,12 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n>  \tclosedir(dir);\n>  \n>  \tstrbuf_setlen(path, original_len);\n> -\tif (!ret && !keep_toplevel && !kept_down)\n> -\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n> -\telse if (kept_up)\n> +\tif (!ret && !keep_toplevel && !kept_down) {\n> +\t\tif (the_cwd && !strcmp(the_cwd, path->buf))\n> +\t\t\tret = -1; /* Do not remove current working directory */\n> +\t\telse\n> +\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n> +\t} else if (kept_up)\n>  \t\t/*\n>  \t\t * report the uplevel that it is not an error that we\n>  \t\t * did not rmdir() our directory.\n> @@ -3327,6 +3330,8 @@ int remove_path(const char *name)\n>  \t\tslash = dirs + (slash - name);\n>  \t\tdo {\n>  \t\t\t*slash = '\\0';\n> +\t\t\tif (the_cwd && !strcmp(the_cwd, dirs))\n> +\t\t\t\tbreak;\n>  \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n>  \t\tfree(dirs);\n>  \t}\n\nI don't think it's appropriate to implement user-facing concern (don't remove \ncwd because this will confuse users) in library functions like remove_path().\nremove_path() has other callers e.g. midx.c, and possible future callers e.g. \nwe're working on adding a command to delete corrupted commit-graphs and this \nlibrary function would be extremely handy.\n\nIt seems more appropriate to check the_cwd from builtin/add.c and builtin/rm.c \ninstead.\n"},{"id":"442077","messageId":"CABPp-BE0Bcimwr1wwcnnh+6apx7r114Oqnu=QDgKEn6VAHAtFg@mail.gmail.com","threadId":"56952","inReplyTo":"20211123003958.3978-1-chooglen@google.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T01:19:15Z","receivedAt":"2021-11-23T01:19:30Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 22, 2021 at 4:40 PM Glen Choo <chooglen@google.com> wrote:\n>\n> > @@ -3259,9 +3259,12 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n> >       closedir(dir);\n> >\n> >       strbuf_setlen(path, original_len);\n> > -     if (!ret && !keep_toplevel && !kept_down)\n> > -             ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n> > -     else if (kept_up)\n> > +     if (!ret && !keep_toplevel && !kept_down) {\n> > +             if (the_cwd && !strcmp(the_cwd, path->buf))\n> > +                     ret = -1; /* Do not remove current working directory */\n> > +             else\n> > +                     ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n> > +     } else if (kept_up)\n> >               /*\n> >                * report the uplevel that it is not an error that we\n> >                * did not rmdir() our directory.\n> > @@ -3327,6 +3330,8 @@ int remove_path(const char *name)\n> >               slash = dirs + (slash - name);\n> >               do {\n> >                       *slash = '\\0';\n> > +                     if (the_cwd && !strcmp(the_cwd, dirs))\n> > +                             break;\n> >               } while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n> >               free(dirs);\n> >       }\n>\n> I don't think it's appropriate to implement user-facing concern (don't remove\n> cwd because this will confuse users) in library functions like remove_path().\n> remove_path() has other callers e.g. midx.c, and possible future callers e.g.\n> we're working on adding a command to delete corrupted commit-graphs and this\n> library function would be extremely handy.\n\nI think we'd want this code change for those cases too.  Said another\nway, why wouldn't these callers want to avoid deleting the original\ncurrent working directory of the git process (which is likely still\nthe current working directory of the parent process)?  Deleting that\ndirectory causes problems regardless of whether it's a user-facing\ncommand (rm, merge, stash, etc.) or something more internal (midx or\ncommit-graphs stuff being called by gc) that is doing the deleting.\n\nPutting it in this helper function means we protect all current and\nfuture callers without developers having to remember which\n\"remove_path()\" variant they need and why.\n\n> It seems more appropriate to check the_cwd from builtin/add.c and builtin/rm.c\n> instead.\n\nNot sure how you determined that those two paths are affected or that\nthose are the only two.\n"},{"id":"442081","messageId":"CABPp-BEJvJSj7oUFeZYbhNmMbqfT7a-L+iQfoob-nHcPcsfiqA@mail.gmail.com","threadId":"56952","inReplyTo":"211121.86bl2d1jo3.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 6/8] clean: do not attempt to remove current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T01:28:29Z","receivedAt":"2021-11-23T01:28:45Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Nov 21, 2021 at 9:51 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > +             int prefixlen = prefix ? strlen(prefix) : 0;\n>\n> nit: s/int/size_t/\n\nThanks; will fix.\n"},{"id":"442083","messageId":"CABPp-BEp3OL7F2J_LzqtC-x-8pBUPO8ZR1fTx_6XbqZeOH1kRw@mail.gmail.com","threadId":"56952","inReplyTo":"211121.867dd11jbl.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T01:45:45Z","receivedAt":"2021-11-23T01:46:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Nov 21, 2021 at 9:59 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > +test_expect_failure 'checkout fails if cwd needs to be removed' '\n> > +     git checkout foo/bar/baz &&\n> > +     test_when_finished \"git clean -fdx\" &&\n> > +\n> > +     mkdir dirORfile &&\n> > +     (\n> > +             cd dirORfile &&\n> > +\n> > +             test_must_fail git checkout fd_conflict 2>../error &&\n> > +             grep \"Refusing to remove the current working directory\" ../error\n> > +     ) &&\n> > +\n> > +     test_path_is_dir dirORfile\n>\n>\n> I'd find this & the rest of this series much easier to understand if we\n> started out by positively asserting the current behavior here, and\n> didn't test_cmp/grep for erro r messages that don't exist anymore.\n\nYeah, this is my fault for my bad commit message.  I stated I was\nadding tests checking for the problems of interest, making it sound\nlike I was testing existing behavior, but I should have stated I was\nadding tests with the behavior we'd prefer to have (i.e. basically a\ntest-driven-development) setup.\n\nAlso, there really wouldn't need to be so many tests for describing\nthe existing behavior.  It's basically just `git\n$OPERATION_THAT_REMOVES_CWD_AS_SIDE_EFFECT` followed by nearly any\nother git command will cause the second and later commands to fail\nwith:\n\n```\nshell-init: error retrieving current directory: getcwd: cannot access\nparent directories: No such file or directory\nfatal: Unable to read current working directory: No such file or directory\n```\n\nHowever, we do need a lot of tests for corrected behavior, because\nthere are so many different codepaths we can follow which will lead to\ndeletion of the current working directory.\n"},{"id":"442087","messageId":"211123.86czmrwqqo.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BEp3OL7F2J_LzqtC-x-8pBUPO8ZR1fTx_6XbqZeOH1kRw@mail.gmail.com","subject":"Re: [PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-23T02:19:47Z","receivedAt":"2021-11-23T02:27:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Nov 22 2021, Elijah Newren wrote:\n\n> On Sun, Nov 21, 2021 at 9:59 AM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> On Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>>\n>> > +test_expect_failure 'checkout fails if cwd needs to be removed' '\n>> > +     git checkout foo/bar/baz &&\n>> > +     test_when_finished \"git clean -fdx\" &&\n>> > +\n>> > +     mkdir dirORfile &&\n>> > +     (\n>> > +             cd dirORfile &&\n>> > +\n>> > +             test_must_fail git checkout fd_conflict 2>../error &&\n>> > +             grep \"Refusing to remove the current working directory\" ../error\n>> > +     ) &&\n>> > +\n>> > +     test_path_is_dir dirORfile\n>>\n>>\n>> I'd find this & the rest of this series much easier to understand if we\n>> started out by positively asserting the current behavior here, and\n>> didn't test_cmp/grep for erro r messages that don't exist anymore.\n>\n> Yeah, this is my fault for my bad commit message.  I stated I was\n> adding tests checking for the problems of interest, making it sound\n> like I was testing existing behavior, but I should have stated I was\n> adding tests with the behavior we'd prefer to have (i.e. basically a\n> test-driven-development) setup.\n>\n> Also, there really wouldn't need to be so many tests for describing\n> the existing behavior.  It's basically just `git\n> $OPERATION_THAT_REMOVES_CWD_AS_SIDE_EFFECT` followed by nearly any\n> other git command will cause the second and later commands to fail\n> with:\n>\n> ```\n> shell-init: error retrieving current directory: getcwd: cannot access\n> parent directories: No such file or directory\n> fatal: Unable to read current working directory: No such file or directory\n> ```\n>\n> However, we do need a lot of tests for corrected behavior, because\n> there are so many different codepaths we can follow which will lead to\n> deletion of the current working directory.\n\nCurrently if I do e.g.:\n\n    git checkout master\n    git clean -dxf\n    cd perl\n    git checkout v0.99\n    cd ../\n    git clean -dxfn\n\nNothing breaks and I don't end up with an empty perl/ to remove. With\nthese patches we'd either die on the \"checkout\" (I think) keep the\n\"perl\" and have an empty perl/ to report in the \"git clean -dxfn\" at the\nend (I'm not sure which, I forgot and haven't re-read this series just\nnow).\n\nI think changing it anyway might be justifiable, but changing the\nbehavior of things like that tickles my spidey sense a bit. I.e. I can\nsee people having written scripts like that which would break (it's\noften easier to cd around after globbing than staying at the top-level,\nthen jump back).\n\nSo I wonder (especially with Glen's comment in\n<20211123003958.3978-1-chooglen@google.com>) if this is being done at\nthe right API level. E.g. maybe it would be better for some commands to\nease into this with an advise() or warning() and not a die() or error(),\nor have the die() be in the likes of \"git switch\" but not \"reset\n--hard\".\n\nOr maybe not, just food for thought...\n"},{"id":"442090","messageId":"CABPp-BH-MtGLAk3vkFCXr8sB3KmMEp98J1wpjO=v6MbXH4tPHg@mail.gmail.com","threadId":"56952","inReplyTo":"211123.86czmrwqqo.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T03:11:01Z","receivedAt":"2021-11-23T03:11:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 22, 2021 at 6:27 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Mon, Nov 22 2021, Elijah Newren wrote:\n>\n> > On Sun, Nov 21, 2021 at 9:59 AM Ævar Arnfjörð Bjarmason\n> > <avarab@gmail.com> wrote:\n> >>\n> >> On Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n> >>\n> >> > From: Elijah Newren <newren@gmail.com>\n> >>\n> >> > +test_expect_failure 'checkout fails if cwd needs to be removed' '\n> >> > +     git checkout foo/bar/baz &&\n> >> > +     test_when_finished \"git clean -fdx\" &&\n> >> > +\n> >> > +     mkdir dirORfile &&\n> >> > +     (\n> >> > +             cd dirORfile &&\n> >> > +\n> >> > +             test_must_fail git checkout fd_conflict 2>../error &&\n> >> > +             grep \"Refusing to remove the current working directory\" ../error\n> >> > +     ) &&\n> >> > +\n> >> > +     test_path_is_dir dirORfile\n> >>\n> >>\n> >> I'd find this & the rest of this series much easier to understand if we\n> >> started out by positively asserting the current behavior here, and\n> >> didn't test_cmp/grep for erro r messages that don't exist anymore.\n> >\n> > Yeah, this is my fault for my bad commit message.  I stated I was\n> > adding tests checking for the problems of interest, making it sound\n> > like I was testing existing behavior, but I should have stated I was\n> > adding tests with the behavior we'd prefer to have (i.e. basically a\n> > test-driven-development) setup.\n> >\n> > Also, there really wouldn't need to be so many tests for describing\n> > the existing behavior.  It's basically just `git\n> > $OPERATION_THAT_REMOVES_CWD_AS_SIDE_EFFECT` followed by nearly any\n> > other git command will cause the second and later commands to fail\n> > with:\n> >\n> > ```\n> > shell-init: error retrieving current directory: getcwd: cannot access\n> > parent directories: No such file or directory\n> > fatal: Unable to read current working directory: No such file or directory\n> > ```\n> >\n> > However, we do need a lot of tests for corrected behavior, because\n> > there are so many different codepaths we can follow which will lead to\n> > deletion of the current working directory.\n>\n> Currently if I do e.g.:\n>\n>     git checkout master\n>     git clean -dxf\n>     cd perl\n>     git checkout v0.99\n>     cd ../\n>     git clean -dxfn\n>\n> Nothing breaks and I don't end up with an empty perl/ to remove. With\n> these patches we'd either die on the \"checkout\" (I think) keep the\n> \"perl\" and have an empty perl/ to report in the \"git clean -dxfn\" at the\n> end (I'm not sure which, I forgot and haven't re-read this series just\n> now).\n\nYou'd have an empty perl/ left after the checkout, which would be\ncleaned up by your final git clean command.\n\n> I think changing it anyway might be justifiable, but changing the\n> behavior of things like that tickles my spidey sense a bit. I.e. I can\n> see people having written scripts like that which would break (it's\n> often easier to cd around after globbing than staying at the top-level,\n> then jump back).\n\nI disagree this would break any user scripts.  If people expect a 'git\ncheckout' or 'git rebase' to always work, their script is _already_\nbroken.  The presence of any untracked files within the directory\nalready results in a hard error -- we refuse to remove non-empty\ndirectories (unless all files are tracked and unmodified).  This rule\ndeserves a clarification: treat the current working directory as\nnon-empty since the parent process is likely still parked there..\n\nFurther, our own commands are broken/misbehaving due to us not\nerroring out; see e.g.\nhttps://lore.kernel.org/git/xmqqv93n7q1v.fsf@gitster.g/ and its\ngrandparent.  User scripts likely have lurking problems too.\n\n> So I wonder (especially with Glen's comment in\n> <20211123003958.3978-1-chooglen@google.com>) if this is being done at\n> the right API level.\n\nGlen's comment was interesting, but provided no specifics about how\nthe changes I made could cause any possible harm in his case.\nFurther, the fact that others are adding extra places doing cleanup\nsound like additional codepaths that should be protected for the exact\nsame reasons.  I think we absolutely want my changes affecting his new\ncodepaths.\n\n> E.g. maybe it would be better for some commands to\n> ease into this with an advise() or warning() and not a die() or error(),\n> or have the die() be in the likes of \"git switch\" but not \"reset\n> --hard\".\n\nThe commands that don't need to remove the current working directory\nbut just were as a convenience, no longer do and continue on just\nfine.  Commands that need to remove the current working directory in\norder to place a file there will error out, just as they would have\nwhen trying to remove a directory with untracked files.  I see no need\nto ease into anything here.\n\n> Or maybe not, just food for thought...\n\nYou may also be interested in reading more of the other thread I\nlinked to from my cover letter; all these cases were discussed in good\ndetail over there.  For example, look at\nhttps://lore.kernel.org/git/CABPp-BFmU+RaAjq4_0-PSfRgH1Jc63nN0fMuDWk2+iDbdz7CCA@mail.gmail.com/.\nPeff's previous suggestion was to just make the commands error out if\nthey'd normally remove the current working directory and require the\nuser to run from a different directory instead.  My version lightened\nthat requirement so it only errors out if the current working\ndirectory needed to be removed in order to place something else there\n(and if nothing else was needed to be placed there, then just leaving\nthe directory around).\n"},{"id":"442183","messageId":"kl6lmtluka55.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"CABPp-BE0Bcimwr1wwcnnh+6apx7r114Oqnu=QDgKEn6VAHAtFg@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2021-11-23T18:19:18Z","receivedAt":"2021-11-23T18:19:21Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> I don't think it's appropriate to implement user-facing concern (don't remove\n>> cwd because this will confuse users) in library functions like remove_path().\n>> remove_path() has other callers e.g. midx.c, and possible future callers e.g.\n>> we're working on adding a command to delete corrupted commit-graphs and this\n>> library function would be extremely handy.\n>\n> I think we'd want this code change for those cases too.  Said another\n> way, why wouldn't these callers want to avoid deleting the original\n> current working directory of the git process (which is likely still\n> the current working directory of the parent process)?  Deleting that\n> directory causes problems regardless of whether it's a user-facing\n> command (rm, merge, stash, etc.) or something more internal (midx or\n> commit-graphs stuff being called by gc) that is doing the deleting.\n\nI agree that most, possibly all, of our commands should prefer to die\nthan to remove the cwd, but that doesn't justify adding\napplication-level concerns to a general-purpose utility function. Even\nif it sounds overly defensive, having an obviously correct utility\nfunction makes it easier for future authors to know exactly what their\ncode is doing and why. And surely if we're imaginative enough, we can\ndefinitely dream up some possible use cases for remove_path() that don't\nwant this dying behavior e.g. other applications that link to our\nlibraries, or some new merge strategy that may need to remove + restore\nthe cwd.\n\nI'm not going to say that we'll *definitely* need remove_path()\nin its current form, but mixing concerns like this is an invitation to\nunexpected behavior. An (imperfect) example that demonstrates this\nprinciple is https://lore.kernel.org/git/24bffdab139435173712101aaf72f7277298c99d.1632497954.git.gitgitgadget@gmail.com/,\nwhere we made a change to a generic path matching function in order to\nspeed up unpack_trees(), but accidentally ended up breaking gitignore.\n\n> Putting it in this helper function means we protect all current and\n> future callers without developers having to remember which\n> \"remove_path()\" variant they need and why.\n\nCode comments sound like adequate protection to me. Fudging the names a\nlittle..\n\n  /*\n   * Pretend we have utility function that generalizes\n   * check-then-delete (though we probably won't need it).\n   */\n  static int remove_path_conditionally(const char *name, check_path_fn can_delete_path);\n\n  /**\n   * This is identical to remove_path(), except that it will die if\n   * attempting to remove the_cwd. When writing Git commands, prefer\n   * using this over remove_path() so that we don't delete the cwd and\n   * leave the user in a confusing state.\n   */\n  int remove_path_except_cwd(const char *name)\n  {\n    return remove_path_conditionally(name, die_on_cwd);\n  }\n\n  /*\n   * Tries to remove the path with empty directories along it, ignores\n   * ENOENT. Unless you really need to remove the path unconditionally,\n   * consider using remove_path_except_cwd() instead.\n   */\n  int remove_path(const char *name);\n\n>> It seems more appropriate to check the_cwd from builtin/add.c and builtin/rm.c\n>> instead.\n>\n> Not sure how you determined that those two paths are affected or that\n> those are the only two.\n\nTypo: s/add/apply.\n\nI took the example from your test cases:\n\n  diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n  index ff4e7cd89fa..4362e7b15e5 100755\n  --- a/t/t2501-cwd-empty.sh\n  +++ b/t/t2501-cwd-empty.sh\n  @@ -191,7 +191,7 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n    test_path_is_dir dirORfile\n   '\n \n  -test_expect_failure 'rm does not remove cwd incidentally' '\n  +test_expect_success 'rm does not remove cwd incidentally' '\n    test_when_finished \"git reset --hard\" &&\n    git checkout foo/bar/baz &&\n \n  @@ -205,7 +205,7 @@ test_expect_failure 'rm does not remove cwd incidentally' '\n    test_path_is_dir foo\n   '\n \n  -test_expect_failure 'apply does not remove cwd incidentally' '\n  +test_expect_success 'apply does not remove cwd incidentally' '\n    test_when_finished \"git reset --hard\" &&\n    git checkout foo/bar/baz &&\n\nI read this as \"I made these changes in order to make these tests pass\".\nI really like the 'TDD-ish' approach you used in this series; as a\nreader, it gave me a clear idea of the expected outcome of your changes.\nFrom that perspective, the fact that there are certainly untested paths\nwhich are affected takes away some of the benefits of this approach.\n"},{"id":"442195","messageId":"CABPp-BGr9PDTE0q5zev4Ffx19g+hG083zdNShoSdH47VqzT8mw@mail.gmail.com","threadId":"56952","inReplyTo":"kl6lmtluka55.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T19:56:53Z","receivedAt":"2021-11-23T19:57:07Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Nov 23, 2021 at 10:19 AM Glen Choo <chooglen@google.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> I don't think it's appropriate to implement user-facing concern (don't remove\n> >> cwd because this will confuse users) in library functions like remove_path().\n> >> remove_path() has other callers e.g. midx.c, and possible future callers e.g.\n> >> we're working on adding a command to delete corrupted commit-graphs and this\n> >> library function would be extremely handy.\n> >\n> > I think we'd want this code change for those cases too.  Said another\n> > way, why wouldn't these callers want to avoid deleting the original\n> > current working directory of the git process (which is likely still\n> > the current working directory of the parent process)?  Deleting that\n> > directory causes problems regardless of whether it's a user-facing\n> > command (rm, merge, stash, etc.) or something more internal (midx or\n> > commit-graphs stuff being called by gc) that is doing the deleting.\n>\n> I agree that most, possibly all, of our commands should prefer to die\n> than to remove the cwd, but that doesn't justify adding\n> application-level concerns to a general-purpose utility function. Even\n> if it sounds overly defensive, having an obviously correct utility\n> function makes it easier for future authors to know exactly what their\n> code is doing and why. And surely if we're imaginative enough, we can\n> definitely dream up some possible use cases for remove_path() that don't\n> want this dying behavior e.g. other applications that link to our\n> libraries, or some new merge strategy that may need to remove + restore\n> the cwd.\n\nSounds like your objections here are based on a misunderstanding.  I\ntotally agree with you that adding dying behavior to these functions\nwould be wrong.\n\nMy patch doesn't do that.\n\n> I'm not going to say that we'll *definitely* need remove_path()\n> in its current form, but mixing concerns like this is an invitation to\n> unexpected behavior. An (imperfect) example that demonstrates this\n> principle is https://lore.kernel.org/git/24bffdab139435173712101aaf72f7277298c99d.1632497954.git.gitgitgadget@gmail.com/,\n> where we made a change to a generic path matching function in order to\n> speed up unpack_trees(), but accidentally ended up breaking gitignore.\n\nThere's no mixture of concerns; my patch is correcting this library\nfunction to more fully match its documented intent; from dir.h:\n\n    /* tries to remove the path with empty directories along it,\nignores ENOENT */\n    int remove_path(const char *path);\n\nSince the parent process's current working directory is still likely\nparked in that directory, there is a good reason to treat it as\nnon-empty.  Thus the cwd should not be one of those directories\nremoved along with the specified path.  No need to die, just stop\nremoving the leading directories once it hits the cwd (much like it'd\nstop once it hit a directory that had files left in it).\n"},{"id":"442199","messageId":"kl6lzgputxxw.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"CABPp-BGr9PDTE0q5zev4Ffx19g+hG083zdNShoSdH47VqzT8mw@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2021-11-23T20:32:43Z","receivedAt":"2021-11-23T20:33:23Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> I agree that most, possibly all, of our commands should prefer to die\n>> than to remove the cwd, but that doesn't justify adding\n>> application-level concerns to a general-purpose utility function. Even\n>> if it sounds overly defensive, having an obviously correct utility\n>> function makes it easier for future authors to know exactly what their\n>> code is doing and why. And surely if we're imaginative enough, we can\n>> definitely dream up some possible use cases for remove_path() that don't\n>> want this dying behavior e.g. other applications that link to our\n>> libraries, or some new merge strategy that may need to remove + restore\n>> the cwd.\n>\n> Sounds like your objections here are based on a misunderstanding.  I\n> totally agree with you that adding dying behavior to these functions\n> would be wrong.\n>\n> My patch doesn't do that.\n\nAh my mistake, that should be s/die/'stop gently'. Even so, that is not\nat the core of my objection, mixing of concerns is.\n\n>> I'm not going to say that we'll *definitely* need remove_path()\n>> in its current form, but mixing concerns like this is an invitation to\n>> unexpected behavior. An (imperfect) example that demonstrates this\n>> principle is https://lore.kernel.org/git/24bffdab139435173712101aaf72f7277298c99d.1632497954.git.gitgitgadget@gmail.com/,\n>> where we made a change to a generic path matching function in order to\n>> speed up unpack_trees(), but accidentally ended up breaking gitignore.\n>\n> There's no mixture of concerns; my patch is correcting this library\n> function to more fully match its documented intent; from dir.h:\n>\n>     /* tries to remove the path with empty directories along it,\n> ignores ENOENT */\n>     int remove_path(const char *path);\n\nI don't think that there is a mismatch; reading the implementation +\ndocumented intent seems to make it clear that 'emptiness' is defined by\ndirectory contents, not the presence of any processes using it as its\ncurrent working directory.\n\n> Since the parent process's current working directory is still likely\n> parked in that directory, there is a good reason to treat it as\n> non-empty.  Thus the cwd should not be one of those directories\n> removed along with the specified path.  No need to die, just stop\n> removing the leading directories once it hits the cwd (much like it'd\n> stop once it hit a directory that had files left in it).\n\nThis doesn't sound like a typical definition of 'emptiness' to me, but I\ncan accept it if others also find it compelling. IOW if your definition\nof 'emptiness' is compelling enough, then I'll be convinced that there\nis no mixing of concerns and there would be no objection.\n"},{"id":"442202","messageId":"xmqqv90i7cxd.fsf@gitster.g","threadId":"56952","inReplyTo":"kl6lzgputxxw.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-23T21:57:34Z","receivedAt":"2021-11-23T21:57:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> This doesn't sound like a typical definition of 'emptiness' to me, but I\n> can accept it if others also find it compelling. IOW if your definition\n> of 'emptiness' is compelling enough, then I'll be convinced that there\n> is no mixing of concerns and there would be no objection.\n\nFWIW, I do not find it compelling.  I can grant that it might be\nconvenient, but I do not think it is a good idea to explain the\nreason why the directory is protected is because it is \"not empty\".\n\n"},{"id":"442207","messageId":"CABPp-BHn0bE4ZSx25+28GD58sae=FVs63eQW-Fp8zwFAALcKFA@mail.gmail.com","threadId":"56952","inReplyTo":"kl6lzgputxxw.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T23:13:11Z","receivedAt":"2021-11-23T23:13:25Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Nov 23, 2021 at 12:33 PM Glen Choo <chooglen@google.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> I agree that most, possibly all, of our commands should prefer to die\n> >> than to remove the cwd, but that doesn't justify adding\n> >> application-level concerns to a general-purpose utility function. Even\n> >> if it sounds overly defensive, having an obviously correct utility\n> >> function makes it easier for future authors to know exactly what their\n> >> code is doing and why. And surely if we're imaginative enough, we can\n> >> definitely dream up some possible use cases for remove_path() that don't\n> >> want this dying behavior e.g. other applications that link to our\n> >> libraries, or some new merge strategy that may need to remove + restore\n> >> the cwd.\n> >\n> > Sounds like your objections here are based on a misunderstanding.  I\n> > totally agree with you that adding dying behavior to these functions\n> > would be wrong.\n> >\n> > My patch doesn't do that.\n>\n> Ah my mistake, that should be s/die/'stop gently'. Even so, that is not\n> at the core of my objection, mixing of concerns is.\n\nIf I were to introduce a new function, say remove_path_not_cwd(), to\navoid this claimed mixing of concerns, what would that buy us?\n\nI've looked at every single caller of remove_path() in the git\ncodebase.  If I did introduce a new function, as you seem to want, my\nseries would include two more commits: one that would replace _every_\ncall of remove_path() in the codebase with a call to the new function,\nand one that would delete the remove_path() declaration and definition\nin dir.[ch] since they would be unused.  The net effect would be\nmerely forcing git developers to learn a different function name.\n(I'd probably also follow it up later with another commit to rename\nremove_path_not_cwd() to remove_path(), for simplicity, and so I don't\nhave to remember the longer name anymore.)\n\n\nI haven't yet found or heard of any potential callers, even\nhypothetical, that would be harmed by the modified behavior.  Every\ncase suggested so far actually sounds like a good candidate for the\nmodified behavior.\n"},{"id":"442208","messageId":"CABPp-BHUj=3b8xZhD3weBPYuWaVR5MrrM8+KCwhtcB8diZcJyg@mail.gmail.com","threadId":"56952","inReplyTo":"xmqqv90i7cxd.fsf@gitster.g","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-23T23:23:00Z","receivedAt":"2021-11-23T23:23:20Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Nov 23, 2021 at 1:57 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Glen Choo <chooglen@google.com> writes:\n>\n> > This doesn't sound like a typical definition of 'emptiness' to me, but I\n> > can accept it if others also find it compelling. IOW if your definition\n> > of 'emptiness' is compelling enough, then I'll be convinced that there\n> > is no mixing of concerns and there would be no objection.\n>\n> FWIW, I do not find it compelling.  I can grant that it might be\n> convenient, but I do not think it is a good idea to explain the\n> reason why the directory is protected is because it is \"not empty\".\n\nIs the objection to my hand-wavy explanation?  If so, point taken.\nHowever, I'm curious if you're also objecting to my commit message\nand/or the patch as well.\n\nIf your objection also includes my commit message, but not the patch,\nwould the following suit your taste better? :\n\n\"\"\"\nremove_path() was added in 4a92d1bfb784 (Add remove_path: a function to\nremove as much as possible of a path, 2008-09-27) to, as it says, remove\nas much of a path as possible.  Why remove as much as possible?  Well,\nat the time we probably would have said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nHowever, the second reason (and perhaps also the first) do not hold when\nthat empty directory was the current working directory we inherited from\nour parent process.  Leaving the parent process in a deleted directory\ncan cause user confusion when subsequent processes fail: any git\ncommand, for example, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nModify remove_path() so that the empty leading directories it also\ndeletes does not include the current working directory we inherited from\nour parent process.  Also do the same for remove_dir_recursively().\n\"\"\"\n"},{"id":"442215","messageId":"kl6ltug2tmhy.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"CABPp-BHn0bE4ZSx25+28GD58sae=FVs63eQW-Fp8zwFAALcKFA@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2021-11-24T00:39:53Z","receivedAt":"2021-11-24T00:39:58Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> >> I agree that most, possibly all, of our commands should prefer to die\n>> >> than to remove the cwd, but that doesn't justify adding\n>> >> application-level concerns to a general-purpose utility function. Even\n>> >> if it sounds overly defensive, having an obviously correct utility\n>> >> function makes it easier for future authors to know exactly what their\n>> >> code is doing and why. And surely if we're imaginative enough, we can\n>> >> definitely dream up some possible use cases for remove_path() that don't\n>> >> want this dying behavior e.g. other applications that link to our\n>> >> libraries, or some new merge strategy that may need to remove + restore\n>> >> the cwd.\n>> >\n>> > Sounds like your objections here are based on a misunderstanding.  I\n>> > totally agree with you that adding dying behavior to these functions\n>> > would be wrong.\n>> >\n>> > My patch doesn't do that.\n>>\n>> Ah my mistake, that should be s/die/'stop gently'. Even so, that is not\n>> at the core of my objection, mixing of concerns is.\n>\n> If I were to introduce a new function, say remove_path_not_cwd(), to\n> avoid this claimed mixing of concerns, what would that buy us?\n> I've looked at every single caller of remove_path() in the git\n> codebase.  If I did introduce a new function, as you seem to want, my\n> series would include two more commits: one that would replace _every_\n> call of remove_path() in the codebase with a call to the new function,\n> and one that would delete the remove_path() declaration and definition\n> in dir.[ch] since they would be unused.\n\nThere is at least one other possible outcome, which is that\nremove_path() is replaced with remove_path_not_cwd() in all callers that\nobviously want it e.g. builtins, but not replaced in other callers. My\nmental model of this is that the two functions serve two different use\ncases:\n\n1) remove_path(): Remove a path and all empty directories\n2) remove_path_not_cwd(): Remove a path and all empty directories,\n   except cwd\n\n> I haven't yet found or heard of any potential callers, even\n> hypothetical, that would be harmed by the modified behavior.  Every\n> case suggested so far actually sounds like a good candidate for the\n> modified behavior.\n\nI trust that you have considered this change carefully, so I will\ndowngrade my objection to a nitpick. remove_path() seems nice to have as\na low-level function but I certainly can't imagine any non-contrived\nuse cases that *wouldn't* benefit.\n\nTo me, a more compelling argument is that protecting cwd is important\nin order to ensure correctness, and user experience is an incidental\nbenefit. AFAICT that is not the argument you are making, but perhaps\nthere is some correctness benefit as well?\n\n"},{"id":"442219","messageId":"211124.86ee76e4fl.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BGr9PDTE0q5zev4Ffx19g+hG083zdNShoSdH47VqzT8mw@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-24T01:10:04Z","receivedAt":"2021-11-24T01:19:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Nov 23 2021, Elijah Newren wrote:\n\n> On Tue, Nov 23, 2021 at 10:19 AM Glen Choo <chooglen@google.com> wrote:\n>>\n>> Elijah Newren <newren@gmail.com> writes:\n>> [...]\n>> I'm not going to say that we'll *definitely* need remove_path()\n>> in its current form, but mixing concerns like this is an invitation to\n>> unexpected behavior. An (imperfect) example that demonstrates this\n>> principle is https://lore.kernel.org/git/24bffdab139435173712101aaf72f7277298c99d.1632497954.git.gitgitgadget@gmail.com/,\n>> where we made a change to a generic path matching function in order to\n>> speed up unpack_trees(), but accidentally ended up breaking gitignore.\n>\n> There's no mixture of concerns; my patch is correcting this library\n> function to more fully match its documented intent; from dir.h:\n>\n>     /* tries to remove the path with empty directories along it,\n> ignores ENOENT */\n>     int remove_path(const char *path);\n>\n> Since the parent process's current working directory is still likely\n> parked in that directory, there is a good reason to treat it as\n> non-empty.  Thus the cwd should not be one of those directories\n> removed along with the specified path.  No need to die, just stop\n> removing the leading directories once it hits the cwd (much like it'd\n> stop once it hit a directory that had files left in it).\n\nI can buy that we'd pick this new behavior as a worthwhile trade-off,\nbut not that anyone intended for this to be the behavior all along.\n\nI don't think \"a process is sitting in it\" has ever been anyone's idea\nof a \"non-empty directory\". Rather it's what rmdir() returning EEXIST or\nENOTEMPTY maps to.\n\nDoesn't this series also change the behavior of e.g.:\n\n    cd contrib/subtree\n    git rm -r ../subtree\n\nIf so then re the \"spidey sense\" comment I had earlier: There's no rm\ncodepaths or tests changed by this series, so the implementation of\ndoing it at this lower level might be casting too wide a net. Wouldn't\ne.g. changing callers that use \"remove_dir_recursively()\" to use a\n\"remove_dir_recursively_not_cwd()\" (or whatever) be a gentler way of\nintroducing this, and make sure that each step of the way we grok what's\nbeing changed, that there's test coverage etc.\n"},{"id":"442227","messageId":"CABPp-BGZPheDqWD1pXbePOYe2BcT1-TqX-zGVqnNLGtFuXab1g@mail.gmail.com","threadId":"56952","inReplyTo":"211124.86ee76e4fl.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-24T04:35:44Z","receivedAt":"2021-11-24T04:36:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Nov 23, 2021 at 5:19 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Tue, Nov 23 2021, Elijah Newren wrote:\n>\n> > On Tue, Nov 23, 2021 at 10:19 AM Glen Choo <chooglen@google.com> wrote:\n> >>\n> >> Elijah Newren <newren@gmail.com> writes:\n> >> [...]\n> >> I'm not going to say that we'll *definitely* need remove_path()\n> >> in its current form, but mixing concerns like this is an invitation to\n> >> unexpected behavior. An (imperfect) example that demonstrates this\n> >> principle is https://lore.kernel.org/git/24bffdab139435173712101aaf72f7277298c99d.1632497954.git.gitgitgadget@gmail.com/,\n> >> where we made a change to a generic path matching function in order to\n> >> speed up unpack_trees(), but accidentally ended up breaking gitignore.\n> >\n> > There's no mixture of concerns; my patch is correcting this library\n> > function to more fully match its documented intent; from dir.h:\n> >\n> >     /* tries to remove the path with empty directories along it,\n> > ignores ENOENT */\n> >     int remove_path(const char *path);\n> >\n> > Since the parent process's current working directory is still likely\n> > parked in that directory, there is a good reason to treat it as\n> > non-empty.  Thus the cwd should not be one of those directories\n> > removed along with the specified path.  No need to die, just stop\n> > removing the leading directories once it hits the cwd (much like it'd\n> > stop once it hit a directory that had files left in it).\n>\n> I can buy that we'd pick this new behavior as a worthwhile trade-off,\n> but not that anyone intended for this to be the behavior all along.\n>\n> I don't think \"a process is sitting in it\" has ever been anyone's idea\n> of a \"non-empty directory\". Rather it's what rmdir() returning EEXIST or\n> ENOTEMPTY maps to.\n\nYeah, Junio commented on my reasoning in that same paragraph of mine.\nBad reasoning on my part, and you were both right to call it out.\n\nBut that reasoning wasn't the underlying motivation for Peff to\nsuggest the behavior behind this series[1] and this patch, nor the\nrationale Junio used to say that the overall behavioral change behind\nthis series \"makes sense\".[2]\n\n[1] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[2] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/\n\n> Doesn't this series also change the behavior of e.g.:\n>\n>     cd contrib/subtree\n>     git rm -r ../subtree\n\nYes, of course!\n\nBefore:\n\n    $ cd contrib/subtree\n    $ git rm -r -q ../subtree/\n    $ ls -ld ../subtree/\n    ls: cannot access '../subtree/': No such file or directory\n    $ git status --porcelain\n    fatal: Unable to read current working directory: No such file or directory\n    $ git checkout HEAD ../subtree/\n    fatal: Unable to read current working directory: No such file or directory\n\nAfter:\n\n    $ cd contrib/subtree\n    $ git rm -r -q ../subtree/\n    $ ls -ld ../subtree/\n    drwxrwxr-x. 1 newren newren 0 Nov 23 19:18 ../subtree/\n    $ git status --porcelain\n    D  contrib/subtree/.gitignore\n    D  contrib/subtree/COPYING\n    D  contrib/subtree/INSTALL\n    D  contrib/subtree/Makefile\n    D  contrib/subtree/README\n    D  contrib/subtree/git-subtree.sh\n    D  contrib/subtree/git-subtree.txt\n    D  contrib/subtree/t/Makefile\n    D  contrib/subtree/t/t7900-subtree.sh\n    D  contrib/subtree/todo\n    $ git checkout HEAD ../subtree/\n    Updated 10 paths from c557be478e\n\nVery nice fix, don't you think?\n\n\n> If so then re the \"spidey sense\" comment I had earlier: There's no rm\n> codepaths or tests changed by this series,\n\nThat's not correct; I explicitly added a new rm test in the first\npatch in my series.  Further, that same test was modified to mark it\nas passing by this particular patch you are commenting on.\n\n> so the implementation of\n> doing it at this lower level might be casting too wide a net.\n\nI'm getting the vibe that you are assuming I'm changing these two\nfunctions without realizing what places might be calling them;\nbasically, that I'm just flippantly changing them.  Ignoring the\nramifications of such an assumption, if this vibe is correct, then let\nme inform you that I've read over each and every caller (as well as\nsearched for other callers of unlink() and rmdir() throughout the tree\nto see if they needed similar changes).  In my opinion, *each* *and*\n*every* *single* *one* of the calls to remove_path() and\nremove_dir_recursively() should take the behavioral change suggested\nin this patch.\n\nIt's also not clear to me that you understand the point of the change\nbehind the series.  Clearly, I'm not doing well explaining it, but\nhave you read Peff's or Junio's comments on why they thought\nprotecting the_original_cwd makes sense?  Again, see the links [1] and\n[2] above.  I think it'd help me understand how to respond to you\nbetter if you could clarify to me whether you disagree with them, or\nwhether you agree with them but think I've gone wrong in the\nimplementation of their high level explanation somehow in this\nparticular patch.\n\n> e.g. changing callers that use \"remove_dir_recursively()\" to use a\n> \"remove_dir_recursively_not_cwd()\" (or whatever) be a gentler way of\n> introducing this, and make sure that each step of the way we grok what's\n> being changed, that there's test coverage etc.\n\nI'll ask you the same two questions I asked Glen when he suggested\nbasically the same thing; if you can provide an answer to either one\nof my questions that is compelling to me, I'd be totally onboard with\nyour suggested change:\n\n(1) Can you point to any concrete example caller anywhere in the code\ntree (or even provide a future hypothetical caller) that would in fact\nbe harmed by the change in this patch?\n\n...and no, I'm not asking you to do my work for me; I've done that\nsame work -- in fact looking at all callsites -- and came up\nempty-handed.  Since this and your previous email are essentially\nclaiming that I've probably missed things, I think it's a fair\nquestion.\n\n(2) What benefit would there be to introducing these new functions?\n\nIn particular, if I were to introduce these new functions, it would\nlook like this:\n\n  * add new remove_path_not_cwd() and remove_dir_recursively_not_cwd()\nfunction in one patch\n  * convert all relevant callsites to use these new functions in the\nsubsequent patch(es)\n  * delete the existing remove_path() and remove_dir_recursively()\nfunctions for two reasons: (1) they are now unused, and (2) future\npotential callers of these old functions would more than likely\nreintroduce the bugs this series is trying to fix if they were to use\nthem and should be discouraged from doing so\n  * rename {remove_path,remove_dir_recursively}_not_cwd() to remove\nthe \"_not_cwd\" suffix in order to have more memorable and less ugly\nnames\n\nOnce finished, the end result would be identical to this patch.  Well,\nidentical other than taking more patches to get to the same end result\nand using up more reviewer time.  If there's some benefit to taking\nthis circuitous route, though, I'm more than willing to do so.\n"},{"id":"442229","messageId":"xmqqh7c23y3n.fsf@gitster.g","threadId":"56952","inReplyTo":"kl6ltug2tmhy.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-24T05:46:04Z","receivedAt":"2021-11-24T05:46:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> To me, a more compelling argument is that protecting cwd is important\n> in order to ensure correctness, and user experience is an incidental\n> benefit. AFAICT that is not the argument you are making, but perhaps\n> there is some correctness benefit as well?\n\nI doubt there is.\n\nIt would be annoying if the command fails to remove a directory that\nbecomes empty after an operation only because your interactive shell\nthat spawned \"git\" was there, especially if your system allows\ninteractive shell to still sit in an unlinked directory.  After such\na rmdir, `/bin/pwd` or `cd ..` may not work, but then the only thing\nyou need to do to recover from this situation is to run `cd\n/path/to/repository` and we can continue working, without having to\nworry about a leftover directory.\n\nThat may be a more pleasant end-user experience than what is being\nproposed, which forces you (1) to realize that you are in a\ndirectory that was to be removed if you weren't sitting there, and\n(2) to compute how many levels of otherwise empty directories were\nleft because of your presense, and (3) to go up sufficient number of\nlevels and manually run \"rm -fr\" the hierarchy, to recover.\n\nSo, I dunno.  It does make the end-user experience on such a system\nthat allows your cwd to go away equally unpleasant to the end-user\nexperience on a system that does not allow your cwd to go away, so\nwe might be gaining a bit in the consistency department, but as has\nbeen already discussed, \"git\" level protection can only notice the\nprocess immediately above us and we wouldn't be able to pay\nattention to the fact some other unrelated process is using a\ndirectory as its cwd, so even such a consistency argument does not\ngo all that far.\n"},{"id":"442230","messageId":"xmqqa6hu3y3g.fsf@gitster.g","threadId":"56952","inReplyTo":"CABPp-BHUj=3b8xZhD3weBPYuWaVR5MrrM8+KCwhtcB8diZcJyg@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-24T05:46:11Z","receivedAt":"2021-11-24T05:46:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Tue, Nov 23, 2021 at 1:57 PM Junio C Hamano <gitster@pobox.com> wrote:\n>>\n>> Glen Choo <chooglen@google.com> writes:\n>>\n>> > This doesn't sound like a typical definition of 'emptiness' to me, but I\n>> > can accept it if others also find it compelling. IOW if your definition\n>> > of 'emptiness' is compelling enough, then I'll be convinced that there\n>> > is no mixing of concerns and there would be no objection.\n>>\n>> FWIW, I do not find it compelling.  I can grant that it might be\n>> convenient, but I do not think it is a good idea to explain the\n>> reason why the directory is protected is because it is \"not empty\".\n>\n> Is the objection to my hand-wavy explanation?  If so, point taken.\n\nThe objection is against the definition of 'emptiness' Glen\nperceived in your explanation ;-)\n\nA directory is empty when there is no filesystem entity hangs below\nit.  A process can have any directory as its cwd, even an empty one,\nbut the presense of such a process does not make an empty directory\nsuddenly non-empty.  That is the objection.\n\n> However, I'm curious if you're also objecting to my commit message\n> and/or the patch as well.\n>\n> If your objection also includes my commit message, but not the patch,\n> would the following suit your taste better? :\n>\n> \"\"\"\n> remove_path() was added in 4a92d1bfb784 (Add remove_path: a function to\n> remove as much as possible of a path, 2008-09-27) to, as it says, remove\n> as much of a path as possible.  Why remove as much as possible?  Well,\n> at the time we probably would have said something like:\n>\n>   * removing leading directories makes things feel tidy\n>   * removing leading directories doesn't hurt anything so long as they\n>     had no files in them.\n\nI think you meant trailing, but I do not think you need to say it\ntwice---they say pretty much the same thing.  We are removing as\nmany directories that contain no files (i.e. non-directories)\nunderneath to make things tidy, as such a directory serves no useful\npurpose.\n\nI am not saying that an empty directory should not be protected even\nwhen the current process sits there.  I just do not think it is a\ngood idea to call the protection \"we protect a non-empty directory\".\nIt is something else.\n"},{"id":"442237","messageId":"211124.86wnkxdbza.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BGZPheDqWD1pXbePOYe2BcT1-TqX-zGVqnNLGtFuXab1g@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-24T11:14:05Z","receivedAt":"2021-11-24T11:33:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Nov 23 2021, Elijah Newren wrote:\n\n> On Tue, Nov 23, 2021 at 5:19 PM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n\n>> Doesn't this series also change the behavior of e.g.:\n>>\n>>     cd contrib/subtree\n>>     git rm -r ../subtree\n>\n> Yes, of course!\n>\n> Before:\n>\n>     $ cd contrib/subtree\n>     $ git rm -r -q ../subtree/\n>     $ ls -ld ../subtree/\n>     ls: cannot access '../subtree/': No such file or directory\n>     $ git status --porcelain\n>     fatal: Unable to read current working directory: No such file or directory\n>     $ git checkout HEAD ../subtree/\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> After:\n>\n>     $ cd contrib/subtree\n>     $ git rm -r -q ../subtree/\n>     $ ls -ld ../subtree/\n>     drwxrwxr-x. 1 newren newren 0 Nov 23 19:18 ../subtree/\n>     $ git status --porcelain\n>     D  contrib/subtree/.gitignore\n>     D  contrib/subtree/COPYING\n>     D  contrib/subtree/INSTALL\n>     D  contrib/subtree/Makefile\n>     D  contrib/subtree/README\n>     D  contrib/subtree/git-subtree.sh\n>     D  contrib/subtree/git-subtree.txt\n>     D  contrib/subtree/t/Makefile\n>     D  contrib/subtree/t/t7900-subtree.sh\n>     D  contrib/subtree/todo\n>     $ git checkout HEAD ../subtree/\n>     Updated 10 paths from c557be478e\n>\n> Very nice fix, don't you think?\n\nI'd be more sympathetic to this for the \"checkout\" etc. commands, but\nonce I add a \"-f\" to that \"rm\" I'm *really* expecting it to rm the\ndirectory, but it won't anymore because it's in the underlying library\nfunction.\n\nBut if the goal is to get \"git status\" and the like working isn't a much\nmore pointed fix to have setup.c handle the case of getting ENOENT from\ngetcwd() more gracefully. I.e. currently (and even with your patches):\n\n    $ (mkdir blah && cd blah && rmdir ../blah && git status)\n    fatal: Unable to read current working directory: No such file or directory\n\nWhereas if we do e.g.:\n\t\n\tdiff --git a/strbuf.c b/strbuf.c\n\tindex b22e9816559..3f9a957ff9d 100644\n\t--- a/strbuf.c\n\t+++ b/strbuf.c\n\t@@ -600,6 +600,16 @@ int strbuf_getcwd(struct strbuf *sb)\n\t                        return 0;\n\t                }\n\t\n\t+               if (errno == ENOENT){\n\t+                       const char *pwd = getenv(\"PWD\");\n\t+\n\t+                       if (pwd) {\n\t+                               warning(_(\"unable to getcwd(), but can read PWD, limping along with that...\"));\n\t+                               strbuf_addstr(sb, pwd);\n\t+                               return 0;\n\t+                       }\n\t+               }\n\t+\n\t                /*\n\t                 * If getcwd(3) is implemented as a syscall that falls\n\t                 * back to a regular lookup using readdir(3) etc. then\n\nWe'll get:\n\t\n\t$ (mkdir blah && cd blah && rmdir ../blah && GIT_DISCOVERY_ACROSS_FILESYSTEM=1 ~/g/git/git status)\n\twarning: unable to getcwd(), but can read PWD, limping along with that...\n\tOn branch master\n\tYour branch is up to date with 'origin/master'.\n\t\n\tChanges not staged for commit:\n\t  (use \"git add <file>...\" to update what will be committed)\n\t  (use \"git restore <file>...\" to discard changes in working directory)\n\t        modified:   ../strbuf.c\n\t\n\tno changes added to commit (use \"git add\" and/or \"git commit -a\")\n\nI think that getenv(\"PWD\") trick is widely supported, and once we get\npast that we seem OK. The relative path to strbuf.c is even correct.\n\nCurrently you'd need to set GIT_DISCOVERY_ACROSS_FILESYSTEM=1 because we\nrun into another case in setup.c where we're not carrying that ENOENT\nforward, but we could just patch strbuf_getcwd() or that subsequent code\nto handle this edge case.\n\n>> If so then re the \"spidey sense\" comment I had earlier: There's no rm\n>> codepaths or tests changed by this series,\n>\n> That's not correct; I explicitly added a new rm test in the first\n> patch in my series.  Further, that same test was modified to mark it\n> as passing by this particular patch you are commenting on.\n\nSorry about that, I didn't look carefully enough.\n\n>> so the implementation of\n>> doing it at this lower level might be casting too wide a net.\n>\n> I'm getting the vibe that you are assuming I'm changing these two\n> functions without realizing what places might be calling them;\n> basically, that I'm just flippantly changing them.  Ignoring the\n> ramifications of such an assumption, if this vibe is correct[...]\n\nSorry no, I didn't mean to imply that. I snipped the rest, but hopefully\nthis answers the questions you had well enough (and in the time I have\nfor this reply):\n\nI'm not concerned that you didn't research this change well enough, I\njust find it a bit iffy to introduce semantics in git around FS\noperations that don't conform with that of POSIX & the underlying OS. My\n*nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\nI'm in, I'd expect git to do the same.\n\nBut whatever research we do on in-tree users we're left with changing\nbehavior for users in the wild, e.g. a script that cd's to each subdir\nin a repo, inspects something, and if it wants to remove that does an\n\"git rm -r\" of the directory it's in, commits, and expects the repo to\nbe clean afterwards.\n\nI did follow/read at least one of the the original discussions[1] a bit,\nand some earlier discussion I'm vaguely recalling around bisect in a\nsubdir.\n\nIf the underlying goal is to address the UX problem in git of e.g. \"git\nstatus\" and the like hard-dying I wonder if something in the direction\nof the setup.c/strbuf.c change above might be more of a gentle change.\n\nThat approach of a more gentler setup.c also has the benefit of having\ngit work when it ends up in this situation without the git commands\nhaving landed it there, as in the above \"rmdir\" example.\n\nAnyway, I really didn't have time to look at this very carefully. I just\nremember looking into this with bisect/status etc. in the past, and\nthinking that these problems were solvable in those cases, i.e. they\nwere just being overly anal about ENOENT, and not falling back on \"PWD\"\netc.\n\n1. https://lore.kernel.org/git/YS3Tv7UfNkF+adry@coredump.intra.peff.net/\n"},{"id":"442242","messageId":"bf9b547d-f421-49f7-9dc5-cc1f81187127@iee.email","threadId":"56952","inReplyTo":"211124.86wnkxdbza.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2021-11-24T14:33:13Z","receivedAt":"2021-11-24T14:33:28Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 24/11/2021 11:14, Ævar Arnfjörð Bjarmason wrote:\n> I'm not concerned that you didn't research this change well enough, I\n> just find it a bit iffy to introduce semantics in git around FS\n> operations that don't conform with that of POSIX & the underlying OS. My\n> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n> I'm in, I'd expect git to do the same.\nIsn't this the same, conceptually, as trying to remove the root\ndirectory, but from a Git perspective?\n\ni.e. Something along the lines of\nhttps://superuser.com/questions/542978/is-it-possible-to-remove-the-root-directory\n(their answer is 'no' without a special option, default since 2006)\n\nIf I read the arguments correctly, Elijah is saying that Git shouldn't\ndelete it's own root (cwd) directory, and that it is already implicit\nwithin the current Git code.\n\nPhilip\n"},{"id":"442243","messageId":"211124.86sfvld4cl.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"211124.86wnkxdbza.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-24T14:11:22Z","receivedAt":"2021-11-24T14:43:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Nov 24 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> On Tue, Nov 23 2021, Elijah Newren wrote:\n>\n>> On Tue, Nov 23, 2021 at 5:19 PM Ævar Arnfjörð Bjarmason\n>> <avarab@gmail.com> wrote:\n>\n>>> Doesn't this series also change the behavior of e.g.:\n>>>\n>>>     cd contrib/subtree\n>>>     git rm -r ../subtree\n>>\n>> Yes, of course!\n>>\n>> Before:\n>>\n>>     $ cd contrib/subtree\n>>     $ git rm -r -q ../subtree/\n>>     $ ls -ld ../subtree/\n>>     ls: cannot access '../subtree/': No such file or directory\n>>     $ git status --porcelain\n>>     fatal: Unable to read current working directory: No such file or directory\n>>     $ git checkout HEAD ../subtree/\n>>     fatal: Unable to read current working directory: No such file or directory\n>>\n>> After:\n>>\n>>     $ cd contrib/subtree\n>>     $ git rm -r -q ../subtree/\n>>     $ ls -ld ../subtree/\n>>     drwxrwxr-x. 1 newren newren 0 Nov 23 19:18 ../subtree/\n>>     $ git status --porcelain\n>>     D  contrib/subtree/.gitignore\n>>     D  contrib/subtree/COPYING\n>>     D  contrib/subtree/INSTALL\n>>     D  contrib/subtree/Makefile\n>>     D  contrib/subtree/README\n>>     D  contrib/subtree/git-subtree.sh\n>>     D  contrib/subtree/git-subtree.txt\n>>     D  contrib/subtree/t/Makefile\n>>     D  contrib/subtree/t/t7900-subtree.sh\n>>     D  contrib/subtree/todo\n>>     $ git checkout HEAD ../subtree/\n>>     Updated 10 paths from c557be478e\n>>\n>> Very nice fix, don't you think?\n>\n> I'd be more sympathetic to this for the \"checkout\" etc. commands, but\n> once I add a \"-f\" to that \"rm\" I'm *really* expecting it to rm the\n> directory, but it won't anymore because it's in the underlying library\n> function.\n>\n> But if the goal is to get \"git status\" and the like working isn't a much\n> more pointed fix to have setup.c handle the case of getting ENOENT from\n> getcwd() more gracefully. I.e. currently (and even with your patches):\n>\n>     $ (mkdir blah && cd blah && rmdir ../blah && git status)\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> Whereas if we do e.g.:\n> \t\n> \tdiff --git a/strbuf.c b/strbuf.c\n> \tindex b22e9816559..3f9a957ff9d 100644\n> \t--- a/strbuf.c\n> \t+++ b/strbuf.c\n> \t@@ -600,6 +600,16 @@ int strbuf_getcwd(struct strbuf *sb)\n> \t                        return 0;\n> \t                }\n> \t\n> \t+               if (errno == ENOENT){\n> \t+                       const char *pwd = getenv(\"PWD\");\n> \t+\n> \t+                       if (pwd) {\n> \t+                               warning(_(\"unable to getcwd(), but can read PWD, limping along with that...\"));\n> \t+                               strbuf_addstr(sb, pwd);\n> \t+                               return 0;\n> \t+                       }\n> \t+               }\n> \t+\n> \t                /*\n> \t                 * If getcwd(3) is implemented as a syscall that falls\n> \t                 * back to a regular lookup using readdir(3) etc. then\n>\n> We'll get:\n> \t\n> \t$ (mkdir blah && cd blah && rmdir ../blah && GIT_DISCOVERY_ACROSS_FILESYSTEM=1 ~/g/git/git status)\n> \twarning: unable to getcwd(), but can read PWD, limping along with that...\n> \tOn branch master\n> \tYour branch is up to date with 'origin/master'.\n> \t\n> \tChanges not staged for commit:\n> \t  (use \"git add <file>...\" to update what will be committed)\n> \t  (use \"git restore <file>...\" to discard changes in working directory)\n> \t        modified:   ../strbuf.c\n> \t\n> \tno changes added to commit (use \"git add\" and/or \"git commit -a\")\n>\n> I think that getenv(\"PWD\") trick is widely supported, and once we get\n> past that we seem OK. The relative path to strbuf.c is even correct.\n>\n> Currently you'd need to set GIT_DISCOVERY_ACROSS_FILESYSTEM=1 because we\n> run into another case in setup.c where we're not carrying that ENOENT\n> forward, but we could just patch strbuf_getcwd() or that subsequent code\n> to handle this edge case.\n>\n>>> If so then re the \"spidey sense\" comment I had earlier: There's no rm\n>>> codepaths or tests changed by this series,\n>>\n>> That's not correct; I explicitly added a new rm test in the first\n>> patch in my series.  Further, that same test was modified to mark it\n>> as passing by this particular patch you are commenting on.\n>\n> Sorry about that, I didn't look carefully enough.\n>\n>>> so the implementation of\n>>> doing it at this lower level might be casting too wide a net.\n>>\n>> I'm getting the vibe that you are assuming I'm changing these two\n>> functions without realizing what places might be calling them;\n>> basically, that I'm just flippantly changing them.  Ignoring the\n>> ramifications of such an assumption, if this vibe is correct[...]\n>\n> Sorry no, I didn't mean to imply that. I snipped the rest, but hopefully\n> this answers the questions you had well enough (and in the time I have\n> for this reply):\n>\n> I'm not concerned that you didn't research this change well enough, I\n> just find it a bit iffy to introduce semantics in git around FS\n> operations that don't conform with that of POSIX & the underlying OS. My\n> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n> I'm in, I'd expect git to do the same.\n>\n> But whatever research we do on in-tree users we're left with changing\n> behavior for users in the wild, e.g. a script that cd's to each subdir\n> in a repo, inspects something, and if it wants to remove that does an\n> \"git rm -r\" of the directory it's in, commits, and expects the repo to\n> be clean afterwards.\n>\n> I did follow/read at least one of the the original discussions[1] a bit,\n> and some earlier discussion I'm vaguely recalling around bisect in a\n> subdir.\n>\n> If the underlying goal is to address the UX problem in git of e.g. \"git\n> status\" and the like hard-dying I wonder if something in the direction\n> of the setup.c/strbuf.c change above might be more of a gentle change.\n>\n> That approach of a more gentler setup.c also has the benefit of having\n> git work when it ends up in this situation without the git commands\n> having landed it there, as in the above \"rmdir\" example.\n>\n> Anyway, I really didn't have time to look at this very carefully. I just\n> remember looking into this with bisect/status etc. in the past, and\n> thinking that these problems were solvable in those cases, i.e. they\n> were just being overly anal about ENOENT, and not falling back on \"PWD\"\n> etc.\n>\n> 1. https://lore.kernel.org/git/YS3Tv7UfNkF+adry@coredump.intra.peff.net/\n\nI fleshened this out a bit in this WIP change:\nhttps://github.com/avar/git/tree/avar/setup-handle-gone-directory +\ncommit:\nhttps://github.com/avar/git/commit/97968518909eef88edba44973b7885d154b7a273\n\nAs noted there there's some caveats, but so far nothing I spotted that\ncan't be overcome. It's particularly painful to test it because of an\nimplementation detail of our test suite, the bin-wrappers are\nshellscripts, and the very first thing they do is reset $PWD (none of\nwhich happens if you run the real \"git\" binary).\n\nThat's b.t.w. the \"shell-init\" error you noted in\nhttps://lore.kernel.org/git/CABPp-BEp3OL7F2J_LzqtC-x-8pBUPO8ZR1fTx_6XbqZeOH1kRw@mail.gmail.com/,\nit's from the bin-wrapper.\n\nI really wish we didn't have the bin-wrappers...\n"},{"id":"442263","messageId":"xmqqo8691gr8.fsf@gitster.g","threadId":"56952","inReplyTo":"CABPp-BGZPheDqWD1pXbePOYe2BcT1-TqX-zGVqnNLGtFuXab1g@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-24T19:43:39Z","receivedAt":"2021-11-24T19:43:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> Doesn't this series also change the behavior of e.g.:\n>>\n>>     cd contrib/subtree\n>>     git rm -r ../subtree\n>\n> Yes, of course!\n>\n> Before:\n>\n>     $ cd contrib/subtree\n>     $ git rm -r -q ../subtree/\n>     $ ls -ld ../subtree/\n>     ls: cannot access '../subtree/': No such file or directory\n>     $ git status --porcelain\n>     fatal: Unable to read current working directory: No such file or directory\n>     $ git checkout HEAD ../subtree/\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> After:\n>\n>     $ cd contrib/subtree\n>     $ git rm -r -q ../subtree/\n>     $ ls -ld ../subtree/\n>     drwxrwxr-x. 1 newren newren 0 Nov 23 19:18 ../subtree/\n>     $ git status --porcelain\n>     D  contrib/subtree/.gitignore\n>     D  contrib/subtree/COPYING\n>     D  contrib/subtree/INSTALL\n>     D  contrib/subtree/Makefile\n>     D  contrib/subtree/README\n>     D  contrib/subtree/git-subtree.sh\n>     D  contrib/subtree/git-subtree.txt\n>     D  contrib/subtree/t/Makefile\n>     D  contrib/subtree/t/t7900-subtree.sh\n>     D  contrib/subtree/todo\n>     $ git checkout HEAD ../subtree/\n>     Updated 10 paths from c557be478e\n>\n> Very nice fix, don't you think?\n\nNot necessarily.  Devil's advocate hat on.\n\nBecause you used \"ls -ld\" in your illustration, we cannot see what\nhappened to contrib/subtree/t/ directory, but presumably that one is\ngone.\n\nNow, after this, if we \"git commit\", making contrib/subtree\ndirectory truly unnecessary, nothing reminds us that we need to\nremove it manually with \"rmdir\" or \"git clean -d\", no?\n\nWith the \"before\" behaviour, the user still can go back to the top\nlevel of the repository by going back to the directory the user was\nin before typing \"cd contrib/subtree\", by saying \"cd -\" and \"git\nstatus\" and everything else would just work, including not having to\nworry about remembering to clean an empty directory contrib/subtree\nthat is no longer needed.\n\n"},{"id":"442264","messageId":"xmqqk0gx1gmz.fsf@gitster.g","threadId":"56952","inReplyTo":"bf9b547d-f421-49f7-9dc5-cc1f81187127@iee.email","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-24T19:46:12Z","receivedAt":"2021-11-24T19:46:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Philip Oakley <philipoakley@iee.email> writes:\n\n> On 24/11/2021 11:14, Ævar Arnfjörð Bjarmason wrote:\n>> I'm not concerned that you didn't research this change well enough, I\n>> just find it a bit iffy to introduce semantics in git around FS\n>> operations that don't conform with that of POSIX & the underlying OS. My\n>> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n>> I'm in, I'd expect git to do the same.\n> Isn't this the same, conceptually, as trying to remove the root\n> directory, but from a Git perspective?\n>\n> i.e. Something along the lines of\n> https://superuser.com/questions/542978/is-it-possible-to-remove-the-root-directory\n> (their answer is 'no' without a special option, default since 2006)\n>\n> If I read the arguments correctly, Elijah is saying that Git shouldn't\n> delete it's own root (cwd) directory, and that it is already implicit\n> within the current Git code.\n\nI do not think it is about protecting \"root\"; the series wants\n\n    cd t/ && git rm -r ../t\n\nto leave an empty directory at 't/', because \"git rm\" was started in\nthat directory.\n"},{"id":"442280","messageId":"CABPp-BETpWU9Rkd6pcxh6+gav2QtYnu_5V8ji_1_3kMnVswp1Q@mail.gmail.com","threadId":"56952","inReplyTo":"211124.86wnkxdbza.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-25T02:48:17Z","receivedAt":"2021-11-25T03:12:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Nov 24, 2021 at 3:33 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Tue, Nov 23 2021, Elijah Newren wrote:\n>\n> > On Tue, Nov 23, 2021 at 5:19 PM Ævar Arnfjörð Bjarmason\n> > <avarab@gmail.com> wrote:\n>\n> >> Doesn't this series also change the behavior of e.g.:\n> >>\n> >>     cd contrib/subtree\n> >>     git rm -r ../subtree\n> >\n> > Yes, of course!\n> >\n> > Before:\n> >\n> >     $ cd contrib/subtree\n> >     $ git rm -r -q ../subtree/\n> >     $ ls -ld ../subtree/\n> >     ls: cannot access '../subtree/': No such file or directory\n> >     $ git status --porcelain\n> >     fatal: Unable to read current working directory: No such file or directory\n> >     $ git checkout HEAD ../subtree/\n> >     fatal: Unable to read current working directory: No such file or directory\n> >\n> > After:\n> >\n> >     $ cd contrib/subtree\n> >     $ git rm -r -q ../subtree/\n> >     $ ls -ld ../subtree/\n> >     drwxrwxr-x. 1 newren newren 0 Nov 23 19:18 ../subtree/\n> >     $ git status --porcelain\n> >     D  contrib/subtree/.gitignore\n> >     D  contrib/subtree/COPYING\n> >     D  contrib/subtree/INSTALL\n> >     D  contrib/subtree/Makefile\n> >     D  contrib/subtree/README\n> >     D  contrib/subtree/git-subtree.sh\n> >     D  contrib/subtree/git-subtree.txt\n> >     D  contrib/subtree/t/Makefile\n> >     D  contrib/subtree/t/t7900-subtree.sh\n> >     D  contrib/subtree/todo\n> >     $ git checkout HEAD ../subtree/\n> >     Updated 10 paths from c557be478e\n> >\n> > Very nice fix, don't you think?\n>\n> I'd be more sympathetic to this for the \"checkout\" etc. commands, but\n> once I add a \"-f\" to that \"rm\" I'm *really* expecting it to rm the\n> directory, but it won't anymore because it's in the underlying library\n> function.\n\nPlease note that the command `git rm -rf dir` does not mean remove\n`dir`.  It means forcibly and recursively remove all tracked paths\nmatching the pathspec `dir`.  There is certainly a difference, because\nwhile it sometimes might end up deleting `dir`, it also might only\ndelete a subset of dir, and in some cases -- due to our added\nconveniences -- it may also delete things outside of dir/.  It's been\nthat way for a decade and a half.  As such, the complaint that \"`git\nrm -rf dir` sometimes doesn't rm the directory\" is not something new\nbeing introduced by this series; that was already a possibility.  Yes,\nI'm adding another case where that happens, but it's not some entirely\nnew qualitative difference.  More on this below.\n\n> But if the goal is to get \"git status\" and the like working isn't a much\n> more pointed fix to have setup.c handle the case of getting ENOENT from\n> getcwd() more gracefully. I.e. currently (and even with your patches):\n>\n>     $ (mkdir blah && cd blah && rmdir ../blah && git status)\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> Whereas if we do e.g.:\n>\n>         diff --git a/strbuf.c b/strbuf.c\n>         index b22e9816559..3f9a957ff9d 100644\n>         --- a/strbuf.c\n>         +++ b/strbuf.c\n>         @@ -600,6 +600,16 @@ int strbuf_getcwd(struct strbuf *sb)\n>                                 return 0;\n>                         }\n>\n>         +               if (errno == ENOENT){\n>         +                       const char *pwd = getenv(\"PWD\");\n>         +\n>         +                       if (pwd) {\n>         +                               warning(_(\"unable to getcwd(), but can read PWD, limping along with that...\"));\n>         +                               strbuf_addstr(sb, pwd);\n>         +                               return 0;\n>         +                       }\n>         +               }\n>         +\n>                         /*\n>                          * If getcwd(3) is implemented as a syscall that falls\n>                          * back to a regular lookup using readdir(3) etc. then\n>\n> We'll get:\n>\n>         $ (mkdir blah && cd blah && rmdir ../blah && GIT_DISCOVERY_ACROSS_FILESYSTEM=1 ~/g/git/git status)\n>         warning: unable to getcwd(), but can read PWD, limping along with that...\n>         On branch master\n>         Your branch is up to date with 'origin/master'.\n>\n>         Changes not staged for commit:\n>           (use \"git add <file>...\" to update what will be committed)\n>           (use \"git restore <file>...\" to discard changes in working directory)\n>                 modified:   ../strbuf.c\n>\n>         no changes added to commit (use \"git add\" and/or \"git commit -a\")\n>\n> I think that getenv(\"PWD\") trick is widely supported, and once we get\n> past that we seem OK. The relative path to strbuf.c is even correct.\n>\n> Currently you'd need to set GIT_DISCOVERY_ACROSS_FILESYSTEM=1 because we\n> run into another case in setup.c where we're not carrying that ENOENT\n> forward, but we could just patch strbuf_getcwd() or that subsequent code\n> to handle this edge case.\n\nOoh, nifty.  This sounds like a useful complement to this series; I'd\nbe in favor of it.\n\n...\n> I'm not concerned that you didn't research this change well enough, I\n> just find it a bit iffy to introduce semantics in git around FS\n> operations that don't conform with that of POSIX & the underlying OS. My\n> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n> I'm in, I'd expect git to do the same.\n\nThis seems like an odd objection.  Semantics in git around FS\noperations that don't conform with that of POSIX and the underlying OS\nhave been with us for a decade and a half now:\n\nFor example, if we ran the following:\n  mkdir -p a/b/c\n  touch a/b/c/{tracked,untracked}\n  git commit -m \"whatever\" a/b/c/tracked\n  cd a/b\n\nThen we'd see the following:\n\n`rm -rf ../b`: leaves a/ as an empty directory\n`git rm -rf ../b`: does not nuke a/b/; in fact, it leaves\na/b/c/untracked around afterward\n\nAlso, if left a/b/c/untracked out of our setup, then we'd instead see\nthe following:\n\n`rm -rf ../b`: leaves a/ as an empty directory\n`git rm -rf ../b`: not only deletes a/b, but also deletes a/ -- the\nparent of what was specified\n\nAlso, prior to d9b814cc97f1 (Add builtin \"git rm\" command,\n2006-05-19), git rm would never delete the directories, so I guess you\ncan take our non-conforming behavior back even further than a decade\nand a half.\n\n> But whatever research we do on in-tree users we're left with changing\n> behavior for users in the wild, e.g. a script that cd's to each subdir\n> in a repo, inspects something, and if it wants to remove that does an\n> \"git rm -r\" of the directory it's in, commits, and expects the repo to\n> be clean afterwards.\n\nYes, this series is about changing behavior.  It's not an\noptimization, or a code cleanup, or new command or anything.  It's\nvery specifically about git's behavior of deleting leading empty\ndirectories after removing other files being limited to when those\nempty directories are not the original current working directory of\nthe process.\n\nHowever, although I still didn't know of any concrete or even\ntheoretical examples of problems this series would cause, I decided\nafter your email to do a bit more work; perhaps some of it will help:\n\n  * I split this final patch in two, one for each affected function,\nwith much more detailed history and writeup.\n  * remove_dir_recursively() already takes a flags parameter; I added\none for specifying whether the caller wants the traditional behavior\nor this new behavior.\n  * I revisited each and every caller of remove_path() and\nremove_dir_recursively() and re-evaluated each one in case I had\nmissed something.  Of the 32 callers, most are unaffected by the\nchange.  Of those that are affected, most should take it, but I did\nend up finding one that sometimes, in a very specific corner case,\nshould get the old behavior (namely when a submodule is present and\ncwd is within the submodule and you pass all of --git-dir and\n--work-tree and -f to rm when removing the submodule).  I added some\ntestcases and ensured it worked.\n\nSince the flag is used, it's not at risk of being deleted, and thus\nlibrary callers can pick their preferred behavior.  Does that address,\nor at least ameliorate, your concerns?\n\nI'll submit the new series soon.\n\n> I did follow/read at least one of the the original discussions[1] a bit,\n> and some earlier discussion I'm vaguely recalling around bisect in a\n> subdir.\n>\n> If the underlying goal is to address the UX problem in git of e.g. \"git\n> status\" and the like hard-dying I wonder if something in the direction\n> of the setup.c/strbuf.c change above might be more of a gentle change.\n>\n> That approach of a more gentler setup.c also has the benefit of having\n> git work when it ends up in this situation without the git commands\n> having landed it there, as in the above \"rmdir\" example.\n>\n> Anyway, I really didn't have time to look at this very carefully. I just\n> remember looking into this with bisect/status etc. in the past, and\n> thinking that these problems were solvable in those cases, i.e. they\n> were just being overly anal about ENOENT, and not falling back on \"PWD\"\n> etc.\n\nThe fact that git aborts in such directories is one obvious\nmanifestation of the problem, but my commit messages and cover letter\nboth called out that it can happen with non-git commands too.  I\ncertainly think your suggestion is very intriguing and worth pursuing,\nbut I think it complements rather than supplants this series.\n\n> 1. https://lore.kernel.org/git/YS3Tv7UfNkF+adry@coredump.intra.peff.net/\n"},{"id":"442281","messageId":"CABPp-BH_NfkzX5XkhPPpX1_bsR9kWGsBBdcUwikDWsA8jc6KJA@mail.gmail.com","threadId":"56952","inReplyTo":"211124.86sfvld4cl.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-25T02:54:43Z","receivedAt":"2021-11-25T03:14:34Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Nov 24, 2021 at 6:18 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> I fleshened this out a bit in this WIP change:\n> https://github.com/avar/git/tree/avar/setup-handle-gone-directory +\n> commit:\n> https://github.com/avar/git/commit/97968518909eef88edba44973b7885d154b7a273\n\nSweet!\n\n> As noted there there's some caveats, but so far nothing I spotted that\n> can't be overcome. It's particularly painful to test it because of an\n> implementation detail of our test suite, the bin-wrappers are\n> shellscripts, and the very first thing they do is reset $PWD (none of\n> which happens if you run the real \"git\" binary).\n>\n> That's b.t.w. the \"shell-init\" error you noted in\n> https://lore.kernel.org/git/CABPp-BEp3OL7F2J_LzqtC-x-8pBUPO8ZR1fTx_6XbqZeOH1kRw@mail.gmail.com/,\n> it's from the bin-wrapper.\n\nTo be a bit more precise, the error/warning is from bash rather than\nthe bin-wrappers specifically; for example, you can also trigger the\nsame shell-init message with a simple `bash -c 'echo hello'` when your\ncwd has been deleted.\n\n> I really wish we didn't have the bin-wrappers...\n\nI'm curious: is this issue the only reason, or am I reading between\nthe lines correctly that you have additional reasons for that?\n"},{"id":"442285","messageId":"38a120f5c0379daabc1f9730039ff7166037410d.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 1/9] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:08Z","receivedAt":"2021-11-25T08:41:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove empty working directories, especially if\nthey are in the way of placing needed files.  That is normally fine, but\nremoving the current working directory can cause confusion for the user\nwhen they run subsequent commands.  For example, after one git process\nhas removed the current working directory, git status/log/diff will all\nabort with the message:\n\n    fatal: Unable to read current working directory: No such file or directory\n\nSince there are several code paths that can result in the current\nworking directory being removed, add several tests of various different\ncodepaths that check for the behavior we would instead like to see.\nThis include a number of new error messages that we will be adding in\nsubsequent commits as we implement the desired checks.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 255 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 255 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..5dfb456a691\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,255 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\tmkdir subdir &&\n+\ttest_commit subdir/file &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tgit rm subdir/file.t &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile\n+'\n+\n+test_expect_failure 'checkout does not clean cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit checkout init &&\n+\t\tcd ..\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'checkout fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'reset --hard does not clean cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit reset --hard init &&\n+\t\tcd ..\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\ttest_must_fail git reset --hard fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove.*the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'merge does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit merge fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'merge fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git merge fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit cherry-pick fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git cherry-pick fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'rebase does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit rebase foo/bar/baz fd_conflict\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'rebase fails if cwd needs to be removed' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git rebase foo/bar/baz fd_conflict 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'revert does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit revert subdir/file\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'revert fails if cwd needs to be removed' '\n+\tgit checkout fd_conflict &&\n+\tgit revert HEAD &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\t\ttest_must_fail git revert HEAD 2>../error &&\n+\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t) &&\n+\n+\ttest_path_is_dir dirORfile\n+'\n+\n+test_expect_failure 'rm does not remove cwd incidentally' '\n+\ttest_when_finished \"git reset --hard\" &&\n+\tgit checkout foo/bar/baz &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit rm bar/baz.t\n+\t) &&\n+\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_dir foo\n+'\n+\n+test_expect_failure 'apply does not remove cwd incidentally' '\n+\ttest_when_finished \"git reset --hard\" &&\n+\tgit checkout foo/bar/baz &&\n+\n+\t(\n+\t\tcd subdir &&\n+\t\tgit diff subdir/file init | git apply\n+\t) &&\n+\n+\ttest_path_is_missing subdir/file.t &&\n+\ttest_path_is_dir subdir\n+'\n+\n+test_expect_failure 'clean does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir empty &&\n+\tmkdir untracked &&\n+\t>untracked/random &&\n+\t(\n+\t\tcd untracked &&\n+\t\tgit clean -fd -e warnings :/ >../warnings &&\n+\t\tgrep \"Refusing to remove current working directory\" ../warnings\n+\t) &&\n+\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\ttest_path_is_dir untracked\n+'\n+\n+test_expect_failure 'stash does not remove cwd incidentally' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir untracked &&\n+\t>untracked/random &&\n+\t(\n+\t\tcd untracked &&\n+\t\tgit stash --include-untracked &&\n+\t\tgit status\n+\t) &&\n+\n+\ttest_path_is_missing untracked/random &&\n+\ttest_path_is_dir untracked\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"442286","messageId":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.git.git.1637455620.gitgitgadget@gmail.com","subject":"[PATCH v2 0/9] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:07Z","receivedAt":"2021-11-25T08:41:23Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). Many commands either\nsilently avoid removing directories that are not empty (i.e. those that have\nuntracked or modified files in them)[1], or show an error and abort,\ndepending on which is more appropriate for the command in question. With\nthis series, we augment the reasons to avoid removing directories to include\nnot just has-untracked-or-modified-files, but also to avoid removing the\noriginal_cwd as well.\n\nPeff and Junio provided some good pros/cons, if it helps:\n\n * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\nhttps://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\nChanges since v1:\n\n * clarified multiple commit messages\n * renamed the_cwd to startup_info->original_cwd to make it clearer that\n   it's our parent process'es cwd that really matters, which we inherited at\n   program startup. Also pulls it out of the global namespace.\n * Normalize the path for startup_info->original_cwd, and ensure that it's\n   actually the original cwd even if -C is passed to git.\n * small code cleanups suggested by René and Ævar\n * split the final patch, which got the most comments into two, one for each\n   function being modified; significantly extending the first of the two\n   commit messages with a lot of history\n * no longer has a content conflict with so/stash-staged\n * add another value for the flags parameter that remove_dir_recursively()\n   takes so that it can opt into either the old or the new behavior. Use\n   that for the one special corner case I could find where it matters, and\n   add a few tests around it to highlight the utility of the flag.\n\nElijah Newren (9):\n  t2501: add various tests for removing the current working directory\n  setup: introduce startup_info->original_cwd\n  unpack-trees: refuse to remove startup_info->original_cwd\n  unpack-trees: add special cwd handling\n  symlinks: do not include startup_info->original_cwd in dir removal\n  clean: do not attempt to remove startup_info->original_cwd\n  stash: do not attempt to remove startup_info->original_cwd\n  dir: avoid incidentally removing the original_cwd in remove_path()\n  dir: new flag to remove_dir_recurse() to spare the original_cwd\n\n builtin/clean.c      |  47 +++++--\n builtin/rm.c         |   3 +-\n builtin/stash.c      |   6 +-\n cache.h              |   1 +\n dir.c                |  15 ++-\n dir.h                |   9 +-\n git.c                |   2 +\n setup.c              |  49 +++++++\n symlinks.c           |   8 +-\n t/t2501-cwd-empty.sh | 297 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  30 ++++-\n unpack-trees.h       |   1 +\n 12 files changed, 446 insertions(+), 22 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v2\nPull-Request: https://github.com/git/git/pull/1140\n\nRange-diff vs v1:\n\n  1:  0b71996a3b4 !  1:  38a120f5c03 t2501: add various tests for removing the current working directory\n     @@ Commit message\n          Numerous commands will remove empty working directories, especially if\n          they are in the way of placing needed files.  That is normally fine, but\n          removing the current working directory can cause confusion for the user\n     -    when they run subsequent commands.  Add some tests checking for such\n     -    problems.\n     +    when they run subsequent commands.  For example, after one git process\n     +    has removed the current working directory, git status/log/diff will all\n     +    abort with the message:\n     +\n     +        fatal: Unable to read current working directory: No such file or directory\n     +\n     +    Since there are several code paths that can result in the current\n     +    working directory being removed, add several tests of various different\n     +    codepaths that check for the behavior we would instead like to see.\n     +    This include a number of new error messages that we will be adding in\n     +    subsequent commits as we implement the desired checks.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n  2:  7b0c665fb75 <  -:  ----------- repository, setup: introduce the_cwd\n  -:  ----------- >  2:  f6129a8ac9c setup: introduce startup_info->original_cwd\n  3:  94bf468be09 !  3:  e74975e83cc unpack-trees: refuse to remove the current working directory\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    unpack-trees: refuse to remove the current working directory\n     +    unpack-trees: refuse to remove startup_info->original_cwd\n      \n          In the past, when a directory needs to be removed to make room for a\n          file, we have always errored out when that directory contains any\n          untracked (but not ignored) files.  Add an extra condition on that: also\n     -    error out if the directory is the current working directory.\n     +    error out if the directory is the current working directory we inherited\n     +    from our parent process.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ unpack-trees.c: static int verify_clean_subdirectory(const struct cache_entry *c\n       \tif (i)\n       \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n      +\n     -+\t/* Do not lose the current working directory. */\n     -+\tif (the_cwd && !strcmp(the_cwd, ce->name))\n     ++\t/* Do not lose startup_info->original_cwd */\n     ++\tif (startup_info->original_cwd &&\n     ++\t    !strcmp(startup_info->original_cwd, ce->name))\n      +\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n      +\n       \treturn cnt;\n  4:  39830fffd45 !  4:  e06806e3a32 unpack-trees: add special cwd handling\n     @@ Commit message\n          code from the previous commit in verify_clean_subdirectory().  However,\n          when we are preserving untracked files, we would rather any error\n          messages about untracked files being in the way take precedence over\n     -    error messages about a subdirectory that happens to be the current\n     -    working directory being in the way.  But in the\n     -    UNPACK_RESET_OVERWRITE_UNTRACKED case, there is no untracked checking to\n     -    be done, so we simply add a special case near the top of\n     -    verify_absent_1.\n     +    error messages about a subdirectory that happens to be the_original_cwd\n     +    being in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\n     +    there is no untracked checking to be done, so we simply add a special\n     +    case near the top of verify_absent_1.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ unpack-trees.c: static int verify_absent_1(const struct cache_entry *ce,\n       \t\treturn 0;\n       \n      +\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n     -+\t\t/* Avoid nuking cwd... */\n     -+\t\tif (the_cwd && !strcmp(the_cwd, ce->name))\n     ++\t\t/* Avoid nuking startup_info->original_cwd... */\n     ++\t\tif (startup_info->original_cwd &&\n     ++\t\t    !strcmp(startup_info->original_cwd, ce->name))\n      +\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n      +\t\t\t\t\t\t ce->name);\n      +\t\t/* ...but nuke anything else. */\n  5:  8a69d2878c9 !  5:  46728f74ea1 symlinks: do not include current working directory in dir removal\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    symlinks: do not include current working directory in dir removal\n     +    symlinks: do not include startup_info->original_cwd in dir removal\n      \n          symlinks has a pair of schedule_dir_for_removal() and\n          remove_scheduled_dirs() functions that ensure that directories made\n          empty by removing other files also themselves get removed.  However, we\n     -    want to exclude the current working directory and leave it around so\n     -    that subsequent git commands (and non-git commands) that the user runs\n     -    afterwards don't cause the user to get confused.\n     +    want to exclude startup_info->original_cwd and leave it around.  This\n     +    avoids the user getting confused by subsequent git commands (and non-git\n     +    commands) that would otherwise report confusing messages about being\n     +    unable to read the current working directory.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## symlinks.c ##\n     -@@ symlinks.c: static int threaded_has_dirs_only_path(struct cache_def *cache, const char *name\n     - \n     - static struct strbuf removal = STRBUF_INIT;\n     - \n     -+static int cant_remove(char *dirname)\n     -+{\n     -+\tif (the_cwd && !strcmp(dirname, the_cwd))\n     -+\t\treturn 1;\n     -+\treturn rmdir(dirname);\n     -+}\n     -+\n     - static void do_remove_scheduled_dirs(int new_len)\n     +@@ symlinks.c: static void do_remove_scheduled_dirs(int new_len)\n       {\n       \twhile (removal.len > new_len) {\n       \t\tremoval.buf[removal.len] = '\\0';\n      -\t\tif (rmdir(removal.buf))\n     -+\t\tif (cant_remove(removal.buf))\n     ++\t\tif ((startup_info->original_cwd &&\n     ++\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n     ++\t\t    rmdir(removal.buf))\n       \t\t\tbreak;\n       \t\tdo {\n       \t\t\tremoval.len--;\n     @@ symlinks.c: void schedule_dir_for_removal(const char *name, int len)\n       {\n       \tint match_len, last_slash, i, previous_slash;\n       \n     -+\tif (the_cwd && !strcmp(name, the_cwd))\n     ++\tif (startup_info->original_cwd &&\n     ++\t    !strcmp(name, startup_info->original_cwd))\n      +\t\treturn;\t/* Do not remove the current working directory */\n      +\n       \tmatch_len = last_slash = i =\n  6:  69bcaf0aab2 !  6:  01ce9444dae clean: do not attempt to remove current working directory\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    clean: do not attempt to remove current working directory\n     +    clean: do not attempt to remove startup_info->original_cwd\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ builtin/clean.c: static const char *msg_skip_git_dir = N_(\"Skipping repository %\n       \n       enum color_clean {\n       \tCLEAN_COLOR_RESET = 0,\n     +@@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n     + {\n     + \tDIR *dir;\n     + \tstruct strbuf quoted = STRBUF_INIT;\n     ++\tstruct strbuf realpath = STRBUF_INIT;\n     ++\tstruct strbuf real_ocwd = STRBUF_INIT;\n     + \tstruct dirent *e;\n     + \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n     + \tstruct string_list dels = STRING_LIST_INIT_DUP;\n      @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n       \tstrbuf_setlen(path, original_len);\n       \n     @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n      -\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n      -\t\t\terrno = saved_errno;\n      -\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n     -+\t\tint prefixlen = prefix ? strlen(prefix) : 0;\n     -+\t\tif (prefix &&\n     -+\t\t    path->len == prefixlen + 2 &&\n     -+\t\t    !strncmp(path->buf, prefix, prefixlen) &&\n     -+\t\t    !strcmp(path->buf + prefixlen, \"./\")) {\n     ++\t\t/*\n     ++\t\t * Normalize path components in path->buf, e.g. change '\\' to\n     ++\t\t * '/' on Windows.\n     ++\t\t */\n     ++\t\tstrbuf_realpath(&realpath, path->buf, 1);\n     ++\n     ++\t\t/*\n     ++\t\t * path and realpath are absolute; for comparison, we want\n     ++\t\t * startup_info->original_cwd to be an absolute path too.  We\n     ++\t\t * can use strbuf_realpath for this.  Also, if original_cwd\n     ++\t\t * started out as the empty string, then it corresponded to\n     ++\t\t * the top of the worktree, which is protected by other means\n     ++\t\t * so we just leave it blank.\n     ++\t\t */\n     ++\t\t if (*startup_info->original_cwd)\n     ++\t\t\t strbuf_realpath(&real_ocwd,\n     ++\t\t\t\t\t startup_info->original_cwd, 1);\n     ++\n     ++\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n      +\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n       \t\t\t*dir_gone = 0;\n      -\t\t\tret = 1;\n     @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n       \t\t}\n       \t}\n       \n     +@@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n     + \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n     + \t}\n     + out:\n     ++\tstrbuf_release(&realpath);\n     ++\tstrbuf_release(&real_ocwd);\n     + \tstrbuf_release(&quoted);\n     + \tstring_list_clear(&dels, 0);\n     + \treturn ret;\n      \n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_expect_failure 'apply does not remove cwd incidentally' '\n  7:  93005b17c28 !  7:  edec0894ca2 stash: do not attempt to remove current working directory\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    stash: do not attempt to remove current working directory\n     +    stash: do not attempt to remove startup_info->original_cwd\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/stash.c ##\n     -@@ builtin/stash.c: static int create_stash(int argc, const char **argv, const char *prefix)\n     - \treturn ret;\n     - }\n     - \n     --static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,\n     -+static int do_push_stash(const struct pathspec *ps, const char *stash_msg,\n     -+\t\t\t const char *prefix, int quiet,\n     - \t\t\t int keep_index, int patch_mode, int include_untracked)\n     - {\n     - \tint ret = 0;\n      @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n       \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n       \n       \t\t\tcp.git_cmd = 1;\n     -+\t\t\tif (prefix)\n     -+\t\t\t\tstrvec_pushl(&cp.args, \"-C\", prefix, NULL);\n     ++\t\t\tif (startup_info->original_cwd &&\n     ++\t\t\t    *startup_info->original_cwd &&\n     ++\t\t\t    !is_absolute_path(startup_info->original_cwd))\n     ++\t\t\t\tcp.dir = startup_info->original_cwd;\n       \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n      -\t\t\t\t     \"--quiet\", \"-d\", NULL);\n      +\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n       \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n       \t\t\t\tstrvec_push(&cp.args, \"-x\");\n       \t\t\tif (run_command(&cp)) {\n     -@@ builtin/stash.c: static int push_stash(int argc, const char **argv, const char *prefix,\n     - \t\tdie(_(\"--pathspec-file-nul requires --pathspec-from-file\"));\n     - \t}\n     - \n     --\treturn do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,\n     --\t\t\t     include_untracked);\n     -+\treturn do_push_stash(&ps, stash_msg, prefix, quiet, keep_index,\n     -+\t\t\t     patch_mode, include_untracked);\n     - }\n     - \n     - static int save_stash(int argc, const char **argv, const char *prefix)\n     -@@ builtin/stash.c: static int save_stash(int argc, const char **argv, const char *prefix)\n     - \t\tstash_msg = strbuf_join_argv(&stash_msg_buf, argc, argv, ' ');\n     - \n     - \tmemset(&ps, 0, sizeof(ps));\n     --\tret = do_push_stash(&ps, stash_msg, quiet, keep_index,\n     -+\tret = do_push_stash(&ps, stash_msg, prefix, quiet, keep_index,\n     - \t\t\t    patch_mode, include_untracked);\n     - \n     - \tstrbuf_release(&stash_msg_buf);\n      \n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'clean does not remove cwd incidentally' '\n  8:  a5528cbb14d !  8:  1815f18592b dir: avoid removing the current working directory\n     @@ Metadata\n      Author: Elijah Newren <newren@gmail.com>\n      \n       ## Commit message ##\n     -    dir: avoid removing the current working directory\n     +    dir: avoid incidentally removing the original_cwd in remove_path()\n      \n     -    dir has a convenient remove_path() helper that will both remove a file\n     -    in a directory and remove its containing directory if it becomes empty\n     -    as a result of the removal, recursing all the way up.  However, we do\n     -    not want the current working directory to be removed, even if it becomes\n     -    empty.\n     +    Modern git often tries to avoid leaving empty directories around when\n     +    removing files.  Originally, it did not bother.  This behavior started\n     +    with commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\n     +    directories, 2005-11-19), stating the reason simply as:\n      \n     -    dir also has a remove_dir_recursively() function which appears to mostly\n     -    be used to remove metadata directories or temporary directories or\n     -    submodules or worktrees.  I am not sure if it needs to be protected\n     -    against removing the current working directory, but did so for good\n     -    measure.\n     +        When the last file in a directory is removed as the result of a\n     +        merge, try to rmdir the now-empty directory.\n     +\n     +    This was reimplemented in C and renamed to remove_path() in commit\n     +    e1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\n     +    internal to merge-recursive.\n     +\n     +    This trend towards removing leading empty directories continued with\n     +    commit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\n     +    stated the reasoning as:\n     +\n     +        The other question is what to do with leading directories. The old\n     +        \"git rm\" script didn't do anything, which is somewhat inconsistent.\n     +        This one will actually clean up directories that have become empty\n     +        as a result of removing the last file, but maybe we want to have a\n     +        flag to decide the behaviour?\n     +\n     +    remove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\n     +    function to remove as much as possible of a path, 2008-09-27), because\n     +    it was noted that we had two separate implementations of the same idea\n     +    AND both were buggy.  It described the purpose of the function as\n     +\n     +        a function to remove as much as possible of a path\n     +\n     +    Why remove as much as possible?  Well, at the time we probably would\n     +    have said something like:\n     +\n     +      * removing leading directories makes things feel tidy\n     +      * removing leading directories doesn't hurt anything so long as they\n     +        had no files in them.\n     +\n     +    But I don't believe those reasons hold when the empty directory happens\n     +    to be the current working directory we inherited from our parent\n     +    process.  Leaving the parent process in a deleted directory can cause\n     +    user confusion when subsequent processes fail: any git command, for\n     +    example, will immediately fail with\n     +\n     +        fatal: Unable to read current working directory: No such file or directory\n     +\n     +    Other commands may similarly get confused.  Modify remove_path() so that\n     +    the empty leading directories it also deletes does not include the\n     +    current working directory we inherited from our parent process.  I have\n     +    looked through every caller of remove_path() in the current codebase to\n     +    make sure that all should take this change.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## dir.c ##\n     -@@ dir.c: static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n     - \tclosedir(dir);\n     - \n     - \tstrbuf_setlen(path, original_len);\n     --\tif (!ret && !keep_toplevel && !kept_down)\n     --\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n     --\telse if (kept_up)\n     -+\tif (!ret && !keep_toplevel && !kept_down) {\n     -+\t\tif (the_cwd && !strcmp(the_cwd, path->buf))\n     -+\t\t\tret = -1; /* Do not remove current working directory */\n     -+\t\telse\n     -+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n     -+\t} else if (kept_up)\n     - \t\t/*\n     - \t\t * report the uplevel that it is not an error that we\n     - \t\t * did not rmdir() our directory.\n      @@ dir.c: int remove_path(const char *name)\n       \t\tslash = dirs + (slash - name);\n       \t\tdo {\n       \t\t\t*slash = '\\0';\n     -+\t\t\tif (the_cwd && !strcmp(the_cwd, dirs))\n     ++\t\t\tif (startup_info->original_cwd &&\n     ++\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n      +\t\t\t\tbreak;\n       \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n       \t\tfree(dirs);\n       \t}\n      \n     + ## dir.h ##\n     +@@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n     +  */\n     + int remove_dir_recursively(struct strbuf *path, int flag);\n     + \n     +-/* tries to remove the path with empty directories along it, ignores ENOENT */\n     ++/*\n     ++ * Tries to remove the path, along with leading empty directories so long as\n     ++ * those empty directories are not startup_info->original_cwd.  Ignores\n     ++ * ENOENT.\n     ++ */\n     + int remove_path(const char *path);\n     + \n     + int fspathcmp(const char *a, const char *b);\n     +\n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'revert fails if cwd needs to be removed' '\n       \ttest_path_is_dir dirORfile\n  -:  ----------- >  9:  adaad7aeaac dir: new flag to remove_dir_recurse() to spare the original_cwd\n\n-- \ngitgitgadget\n"},{"id":"442287","messageId":"f6129a8ac9c3d052fb7fb508a62d4eedb8d9ed57.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:09Z","receivedAt":"2021-11-25T08:41:23Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands run from that directory to get confused and fail with a message\nabout being unable to read the current working directory:\n\n    $ git status\n    fatal: Unable to read current working directory: No such file or directory\n\nNon-git commands likely have similar warnings or even errors, e.g.\n\n    $ bash -c 'echo hello'\n    shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n    hello\n\nThis confuses end users, particularly since the command they get the\nerror from is not the one that caused the problem; the problem came from\nthe side-effect of some previous command.\n\nWe would like to avoid removing the current working directory of our\nparent process; towards this end, introduce a new variable,\nstartup_info->original_cwd, that tracks the current working directory\nthat we inherited from our parent process.  For convenience of later\ncomparisons, we prefer that this new variable store a path relative to\nthe toplevel working directory (thus much like 'prefix'), except without\nthe trailing slash.\n\nSubsequent commits will make use of this new variable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n cache.h |  1 +\n git.c   |  2 ++\n setup.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 52 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex eba12487b99..d7903c65b57 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1834,6 +1834,7 @@ void overlay_tree_on_index(struct index_state *istate,\n struct startup_info {\n \tint have_repository;\n \tconst char *prefix;\n+\tconst char *original_cwd;\n };\n extern struct startup_info *startup_info;\n \ndiff --git a/git.c b/git.c\nindex 5ff21be21f3..2c98ab48936 100644\n--- a/git.c\n+++ b/git.c\n@@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n \n \ttrace_command_performance(argv);\n \n+\tstartup_info->original_cwd = xgetcwd();\n+\n \t/*\n \t * \"git-xxxx\" is the same as \"git xxxx\", but we obviously:\n \t *\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..f30657723ea 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -432,6 +432,54 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static void setup_original_cwd(void)\n+{\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tconst char *worktree = NULL;\n+\tint offset = -1;\n+\n+\t/*\n+\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n+\t * we're an auxiliary tool like git-remote-http or test-tool.\n+\t */\n+\tif (!startup_info->original_cwd)\n+\t\treturn;\n+\n+\t/*\n+\t * startup_info->original_cwd points to the current working\n+\t * directory we inherited from our parent process, which is a\n+\t * directory we want to avoid incidentally removing.\n+\t *\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n+\t *\n+\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+\t * but differs in two ways:\n+\t *   - prefix has a trailing '/'\n+\t *   - if the user passes '-C' to git, that modifies the prefix but\n+\t *     not startup_info->original_cwd.\n+\t */\n+\n+\t/* Normalize the directory */\n+\tstrbuf_realpath(&tmp, startup_info->original_cwd, 1);\n+\tfree((char*)startup_info->original_cwd);\n+\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n+\n+\t/* Find out if this is in the worktree */\n+\tworktree = get_git_work_tree();\n+\tif (worktree)\n+\t\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n+\tif (offset >= 0) {\n+\t\t/*\n+\t\t * original_cwd was inside worktree; precompose it just as\n+\t\t * we do prefix so that built up paths will match\n+\t\t */\n+\t\tstartup_info->original_cwd = \\\n+\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n+\t\t\t\t\t\t    + offset);\n+\t}\n+}\n+\n static int read_worktree_config(const char *var, const char *value, void *vdata)\n {\n \tstruct repository_format *data = vdata;\n@@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n+\tsetup_original_cwd();\n \n \tstrbuf_release(&dir);\n \tstrbuf_release(&gitdir);\n-- \ngitgitgadget\n\n"},{"id":"442288","messageId":"e74975e83cc7a11b8f0378d59a8c2c4a97d3aa50.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 3/9] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:10Z","receivedAt":"2021-11-25T08:41:27Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory we inherited\nfrom our parent process.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 10 +++++-----\n unpack-trees.c       | 17 +++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 19 insertions(+), 9 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 5dfb456a691..212676d71c3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -38,7 +38,7 @@ test_expect_failure 'checkout does not clean cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'checkout fails if cwd needs to be removed' '\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -95,7 +95,7 @@ test_expect_failure 'merge does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'merge fails if cwd needs to be removed' '\n+test_expect_success 'merge fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -122,7 +122,7 @@ test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -149,7 +149,7 @@ test_expect_failure 'rebase does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'rebase fails if cwd needs to be removed' '\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -176,7 +176,7 @@ test_expect_failure 'revert does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'revert fails if cwd needs to be removed' '\n+test_expect_success 'revert fails if cwd needs to be removed' '\n \tgit checkout fd_conflict &&\n \tgit revert HEAD &&\n \ttest_when_finished \"git clean -fdx\" &&\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..6bc16f3a714 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose startup_info->original_cwd */\n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"442289","messageId":"e06806e3a3266fa6fdc973fcd7b479bd395c3023.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 4/9] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:11Z","receivedAt":"2021-11-25T08:41:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the_original_cwd\nbeing in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\nthere is no untracked checking to be done, so we simply add a special\ncase near the top of verify_absent_1.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 13 +++++++++++--\n 2 files changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 212676d71c3..526d8ec2ee3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -67,7 +67,7 @@ test_expect_failure 'reset --hard does not clean cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 6bc16f3a714..5852807d2fb 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking startup_info->original_cwd... */\n+\t\tif (startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"442290","messageId":"46728f74ea175a33ef592319893ed32146229b33.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 5/9] symlinks: do not include startup_info->original_cwd in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:12Z","receivedAt":"2021-11-25T08:41:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude startup_info->original_cwd and leave it around.  This\navoids the user getting confused by subsequent git commands (and non-git\ncommands) that would otherwise report confusing messages about being\nunable to read the current working directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           |  8 +++++++-\n t/t2501-cwd-empty.sh | 12 ++++++------\n 2 files changed, 13 insertions(+), 7 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..c667baa949b 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif ((startup_info->original_cwd &&\n+\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n+\t\t    rmdir(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(name, startup_info->original_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 526d8ec2ee3..b92e1a9bb16 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -24,7 +24,7 @@ test_expect_success setup '\n \tgit commit -m dirORfile\n '\n \n-test_expect_failure 'checkout does not clean cwd incidentally' '\n+test_expect_success 'checkout does not clean cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -53,7 +53,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'reset --hard does not clean cwd incidentally' '\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -82,7 +82,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'merge does not remove cwd incidentally' '\n+test_expect_success 'merge does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -109,7 +109,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n+test_expect_success 'cherry-pick does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -136,7 +136,7 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'rebase does not remove cwd incidentally' '\n+test_expect_success 'rebase does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -163,7 +163,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'revert does not remove cwd incidentally' '\n+test_expect_success 'revert does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442291","messageId":"edec0894ca2a01c7afbc70590efd5ee8a0d1abde.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 7/9] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:14Z","receivedAt":"2021-11-25T08:42:09Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 6 +++++-\n t/t2501-cwd-empty.sh | 2 +-\n 2 files changed, 6 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..50b4875980c 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1485,8 +1485,12 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    *startup_info->original_cwd &&\n+\t\t\t    !is_absolute_path(startup_info->original_cwd))\n+\t\t\t\tcp.dir = startup_info->original_cwd;\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 20e1b6adede..ff4e7cd89fa 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n \ttest_path_is_dir untracked\n '\n \n-test_expect_failure 'stash does not remove cwd incidentally' '\n+test_expect_success 'stash does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442292","messageId":"01ce9444daeafe7a11265956b847f92233f5d028.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 6/9] clean: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:13Z","receivedAt":"2021-11-25T08:42:10Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 47 +++++++++++++++++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  2 +-\n 2 files changed, 39 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..68c8d593c77 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n {\n \tDIR *dir;\n \tstruct strbuf quoted = STRBUF_INIT;\n+\tstruct strbuf realpath = STRBUF_INIT;\n+\tstruct strbuf real_ocwd = STRBUF_INIT;\n \tstruct dirent *e;\n \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n \tstruct string_list dels = STRING_LIST_INIT_DUP;\n@@ -231,16 +235,39 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t/*\n+\t\t * Normalize path components in path->buf, e.g. change '\\' to\n+\t\t * '/' on Windows.\n+\t\t */\n+\t\tstrbuf_realpath(&realpath, path->buf, 1);\n+\n+\t\t/*\n+\t\t * path and realpath are absolute; for comparison, we want\n+\t\t * startup_info->original_cwd to be an absolute path too.  We\n+\t\t * can use strbuf_realpath for this.  Also, if original_cwd\n+\t\t * started out as the empty string, then it corresponded to\n+\t\t * the top of the worktree, which is protected by other means\n+\t\t * so we just leave it blank.\n+\t\t */\n+\t\t if (*startup_info->original_cwd)\n+\t\t\t strbuf_realpath(&real_ocwd,\n+\t\t\t\t\t startup_info->original_cwd, 1);\n+\n+\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -250,6 +277,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n \t}\n out:\n+\tstrbuf_release(&realpath);\n+\tstrbuf_release(&real_ocwd);\n \tstrbuf_release(&quoted);\n \tstring_list_clear(&dels, 0);\n \treturn ret;\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex b92e1a9bb16..20e1b6adede 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -218,7 +218,7 @@ test_expect_failure 'apply does not remove cwd incidentally' '\n \ttest_path_is_dir subdir\n '\n \n-test_expect_failure 'clean does not remove cwd incidentally' '\n+test_expect_success 'clean does not remove cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442293","messageId":"1815f18592b240145000b04d19645f31ea72823e.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 8/9] dir: avoid incidentally removing the original_cwd in remove_path()","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:15Z","receivedAt":"2021-11-25T08:42:12Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nModern git often tries to avoid leaving empty directories around when\nremoving files.  Originally, it did not bother.  This behavior started\nwith commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\ndirectories, 2005-11-19), stating the reason simply as:\n\n    When the last file in a directory is removed as the result of a\n    merge, try to rmdir the now-empty directory.\n\nThis was reimplemented in C and renamed to remove_path() in commit\ne1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\ninternal to merge-recursive.\n\nThis trend towards removing leading empty directories continued with\ncommit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\nstated the reasoning as:\n\n    The other question is what to do with leading directories. The old\n    \"git rm\" script didn't do anything, which is somewhat inconsistent.\n    This one will actually clean up directories that have become empty\n    as a result of removing the last file, but maybe we want to have a\n    flag to decide the behaviour?\n\nremove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\nfunction to remove as much as possible of a path, 2008-09-27), because\nit was noted that we had two separate implementations of the same idea\nAND both were buggy.  It described the purpose of the function as\n\n    a function to remove as much as possible of a path\n\nWhy remove as much as possible?  Well, at the time we probably would\nhave said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nBut I don't believe those reasons hold when the empty directory happens\nto be the current working directory we inherited from our parent\nprocess.  Leaving the parent process in a deleted directory can cause\nuser confusion when subsequent processes fail: any git command, for\nexample, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nOther commands may similarly get confused.  Modify remove_path() so that\nthe empty leading directories it also deletes does not include the\ncurrent working directory we inherited from our parent process.  I have\nlooked through every caller of remove_path() in the current codebase to\nmake sure that all should take this change.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                | 3 +++\n dir.h                | 6 +++++-\n t/t2501-cwd-empty.sh | 4 ++--\n 3 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..97d6b71c872 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3327,6 +3327,9 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/dir.h b/dir.h\nindex 83f46c0fb4c..d6a5d03bec2 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n  */\n int remove_dir_recursively(struct strbuf *path, int flag);\n \n-/* tries to remove the path with empty directories along it, ignores ENOENT */\n+/*\n+ * Tries to remove the path, along with leading empty directories so long as\n+ * those empty directories are not startup_info->original_cwd.  Ignores\n+ * ENOENT.\n+ */\n int remove_path(const char *path);\n \n int fspathcmp(const char *a, const char *b);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex ff4e7cd89fa..4362e7b15e5 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -191,7 +191,7 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n \ttest_path_is_dir dirORfile\n '\n \n-test_expect_failure 'rm does not remove cwd incidentally' '\n+test_expect_success 'rm does not remove cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n@@ -205,7 +205,7 @@ test_expect_failure 'rm does not remove cwd incidentally' '\n \ttest_path_is_dir foo\n '\n \n-test_expect_failure 'apply does not remove cwd incidentally' '\n+test_expect_success 'apply does not remove cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n-- \ngitgitgadget\n\n"},{"id":"442294","messageId":"adaad7aeaac20357f1b83d21085257b8b8fe4591.1637829556.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v2 9/9] dir: new flag to remove_dir_recurse() to spare the original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-25T08:39:16Z","receivedAt":"2021-11-25T08:42:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nremove_dir_recurse(), and its non-static wrapper called\nremove_dir_recursively(), both take flags for modifying its behavior.\nAs with the previous commits, we would generally like to protect\nthe original_cwd, but we want to forced user commands (e.g. 'git rm -rf\n...') or other special cases to remove it.  Add a flag for this purpose.\nAfter reading through every caller of remove_dir_recursively() in the\ncurrent codebase, there was only one that should be adjusted and that\none only in a very unusual circumstance.  Add a pair of new testcases to\nhighlight that very specific case involving submodules && --git-dir &&\n--work-tree.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/rm.c         |  3 ++-\n dir.c                | 12 +++++++++---\n dir.h                |  3 +++\n t/t2501-cwd-empty.sh | 42 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 56 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/rm.c b/builtin/rm.c\nindex 3d0967cdc11..b4132e5d8ee 100644\n--- a/builtin/rm.c\n+++ b/builtin/rm.c\n@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)\n \tif (!index_only) {\n \t\tint removed = 0, gitmodules_modified = 0;\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tint flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;\n \t\tfor (i = 0; i < list.nr; i++) {\n \t\t\tconst char *path = list.entry[i].name;\n \t\t\tif (list.entry[i].is_submodule) {\n \t\t\t\tstrbuf_reset(&buf);\n \t\t\t\tstrbuf_addstr(&buf, path);\n-\t\t\t\tif (remove_dir_recursively(&buf, 0))\n+\t\t\t\tif (remove_dir_recursively(&buf, flag))\n \t\t\t\t\tdie(_(\"could not remove '%s'\"), path);\n \n \t\t\t\tremoved = 1;\ndiff --git a/dir.c b/dir.c\nindex 97d6b71c872..52064345a6b 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tint ret = 0, original_len = path->len, len, kept_down = 0;\n \tint only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);\n \tint keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);\n+\tint purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);\n \tstruct object_id submodule_head;\n \n \tif ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&\n@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (!purge_original_cwd &&\n+\t\t    startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\ndiff --git a/dir.h b/dir.h\nindex d6a5d03bec2..8e02dfb505d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n /* Remove the contents of path, but leave path itself. */\n #define REMOVE_DIR_KEEP_TOPLEVEL 04\n \n+/* Remove the_original_cwd too */\n+#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08\n+\n /*\n  * Remove path and its contents, recursively. flags is a combination\n  * of the above REMOVE_DIR_* constants. Return 0 on success.\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 4362e7b15e5..8da3fb1f5d3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -252,4 +252,46 @@ test_expect_success 'stash does not remove cwd incidentally' '\n \ttest_path_is_dir untracked\n '\n \n+test_expect_success 'rm -r leaves submodule if cwd inside' '\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\ttest_must_fail git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n+\t) &&\n+\n+\ttest_path_is_dir my_submodule\n+'\n+\n+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n+\t) &&\n+\n+\ttest_path_is_missing my_submodule\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"442295","messageId":"211125.86fsrkjz5t.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"38a120f5c0379daabc1f9730039ff7166037410d.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/9] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T10:21:30Z","receivedAt":"2021-11-25T10:43:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Numerous commands will remove empty working directories, especially if\n> they are in the way of placing needed files.  That is normally fine, but\n> removing the current working directory can cause confusion for the user\n> when they run subsequent commands.  For example, after one git process\n> has removed the current working directory, git status/log/diff will all\n> abort with the message:\n>\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> Since there are several code paths that can result in the current\n> working directory being removed, add several tests of various different\n> codepaths that check for the behavior we would instead like to see.\n> This include a number of new error messages that we will be adding in\n> subsequent commits as we implement the desired checks.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  t/t2501-cwd-empty.sh | 255 +++++++++++++++++++++++++++++++++++++++++++\n>  1 file changed, 255 insertions(+)\n>  create mode 100755 t/t2501-cwd-empty.sh\n>\n> [...]\n\nAs noted on v1 (and particularly if we're going to have something like\nmy proposed alternate \"let's not make setup.c die then?\" I really think\nthis should positively assert our existing behavior, and we can then\nupdate this later for the behavior change.\n\nI got that ~working locally, I think you can squash the below in, and\nthen cherry-pick this version on top in the actual change that adds the\n\"Refusing to remove\" behavior change.\n\nI think this really helps to explain the change, and to make sure we\ntest existing behavior.\n\nI added a new test for \"rm\" with the \"-f\" flag, and by the \"~\" in\n\"working\" I mean that this worked as I expected aside from the last\ncouple of tests.\n\nI think that's a bug in your existing test that's hidden by the use of\ntest_expect_failure here. I.e. in the clean test we'll exit with:\n\n    [...]\n    + git clean -fd -e warnings :/\n    warning: failed to remove ./: Invalid argument\n\nA \"bug\" in the sense that this AFAICT would never have removed that\ndirectory anyway since \"clean\" puts out before, but with your change\nwe'll catch that earlier and emit a new \"error\". Also do we need \":/\"\nthere, isn't \".\" more obvious? In any case, the updated test below shows\nthat we already punt out in that case, but perhaps it's incomplete. Will\n\"clean\" remove these directories in other cases already?\n\nThe \"status\" then had to be removed from the \"stash\" test, was it\nleftover debugging cruft?\n\nFinally these are quite repetitive. It would be very welcome to factor\nthese into e.g.:\n\n    test_expect_untracked_dir hard foo/bar/baz -- <command>\n\nWhere we just do the common case of \"if $1 = hard\" we do the setup with\n\"reset --hard\", otherwise the \"git clean -dxf\" etc. Then if it's the\n\"foo/bar/baz\" case add the 3x \"test_path_is_missing\" for that etc.\n\nAnother issue: You have a \"git cmd | other git cmd\" with \"| git apply\"\nthere, should be moved into two split by a && to not potentially hide an\nerror on the LHS of the pipe.\n\nI think there's also probably numerous missing tests here when it comes\nto how other commands behaved before/after we removed the CWD. E.g. the\ncase of:\n\n    # in x/\n    git rm -r ../x\n    git reset ../some-file.txt\n\nWhich I noted in another follow-up to v1, i.e. that fails currently due\nto our path construction in setup.c. I.e. in terms of \"selling\" the\nchange and showing the greater behavior impact, i.e. we from:\n\n    # works\n    git rm -r ../x\n    # fails\n    git reset ../some-file.txt\n\nTo:\n\n    # fails (or keeps x/?)\n    git rm -r ../x\n    # works\n    git reset ../some-file.txt\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 5dfb456a691..f1fb8b4a872 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -24,7 +24,7 @@ test_expect_success setup '\n \tgit commit -m dirORfile\n '\n \n-test_expect_failure 'checkout does not clean cwd incidentally' '\n+test_expect_success 'checkout cleans cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -35,10 +35,10 @@ test_expect_failure 'checkout does not clean cwd incidentally' '\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n-\ttest_path_is_dir foo\n+\ttest_path_is_missing foo\n '\n \n-test_expect_failure 'checkout fails if cwd needs to be removed' '\n+test_expect_success 'checkout if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -46,14 +46,14 @@ test_expect_failure 'checkout fails if cwd needs to be removed' '\n \t(\n \t\tcd dirORfile &&\n \n-\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n-\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t\tgit checkout fd_conflict 2>../error &&\n+\t\tgrep \"Switched to branch\" ../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'reset --hard does not clean cwd incidentally' '\n+test_expect_success 'reset --hard cleans cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_path_is_dir foo/bar &&\n \n@@ -64,10 +64,10 @@ test_expect_failure 'reset --hard does not clean cwd incidentally' '\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n-\ttest_path_is_dir foo\n+\ttest_path_is_missing foo\n '\n \n-test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n+test_expect_success 'reset --hard succeeds if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -75,14 +75,14 @@ test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n \t(\n \t\tcd dirORfile &&\n \n-\t\ttest_must_fail git reset --hard fd_conflict 2>../error &&\n-\t\tgrep \"Refusing to remove.*the current working directory\" ../error\n+\t\tgit reset --hard fd_conflict 2>../error &&\n+\t\ttest_must_be_empty ../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'merge does not remove cwd incidentally' '\n+test_expect_success 'merge removes cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -92,24 +92,24 @@ test_expect_failure 'merge does not remove cwd incidentally' '\n \t) &&\n \n \ttest_path_is_missing subdir/file.t &&\n-\ttest_path_is_dir subdir\n+\ttest_path_is_missing subdir\n '\n \n-test_expect_failure 'merge fails if cwd needs to be removed' '\n+test_expect_success 'merge succeeds if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \tmkdir dirORfile &&\n \t(\n \t\tcd dirORfile &&\n-\t\ttest_must_fail git merge fd_conflict 2>../error &&\n-\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t\tgit merge fd_conflict 2>../error &&\n+\t\ttest_must_be_empty ../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n+test_expect_success 'cherry-pick removes cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -119,24 +119,23 @@ test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n \t) &&\n \n \ttest_path_is_missing subdir/file.t &&\n-\ttest_path_is_dir subdir\n+\ttest_path_is_missing subdir\n '\n \n-test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n+test_expect_success 'cherry-pick suceeds if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \tmkdir dirORfile &&\n \t(\n \t\tcd dirORfile &&\n-\t\ttest_must_fail git cherry-pick fd_conflict 2>../error &&\n-\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t\tgit cherry-pick fd_conflict 2>../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'rebase does not remove cwd incidentally' '\n+test_expect_success 'rebase does removes cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -146,24 +145,23 @@ test_expect_failure 'rebase does not remove cwd incidentally' '\n \t) &&\n \n \ttest_path_is_missing subdir/file.t &&\n-\ttest_path_is_dir subdir\n+\ttest_path_is_missing subdir\n '\n \n-test_expect_failure 'rebase fails if cwd needs to be removed' '\n+test_expect_success 'rebase succeeds if cwd needs to be removed' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \tmkdir dirORfile &&\n \t(\n \t\tcd dirORfile &&\n-\t\ttest_must_fail git rebase foo/bar/baz fd_conflict 2>../error &&\n-\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t\tgit rebase foo/bar/baz fd_conflict 2>../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'revert does not remove cwd incidentally' '\n+test_expect_success 'revert removes cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -173,10 +171,10 @@ test_expect_failure 'revert does not remove cwd incidentally' '\n \t) &&\n \n \ttest_path_is_missing subdir/file.t &&\n-\ttest_path_is_dir subdir\n+\ttest_path_is_missing subdir\n '\n \n-test_expect_failure 'revert fails if cwd needs to be removed' '\n+test_expect_success 'revert succeeds if cwd needs to be removed' '\n \tgit checkout fd_conflict &&\n \tgit revert HEAD &&\n \ttest_when_finished \"git clean -fdx\" &&\n@@ -184,14 +182,13 @@ test_expect_failure 'revert fails if cwd needs to be removed' '\n \tmkdir dirORfile &&\n \t(\n \t\tcd dirORfile &&\n-\t\ttest_must_fail git revert HEAD 2>../error &&\n-\t\tgrep \"Refusing to remove the current working directory\" ../error\n+\t\tgit revert HEAD 2>../error\n \t) &&\n \n-\ttest_path_is_dir dirORfile\n+\ttest_path_is_file dirORfile\n '\n \n-test_expect_failure 'rm does not remove cwd incidentally' '\n+test_expect_success 'rm removes cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n@@ -202,10 +199,24 @@ test_expect_failure 'rm does not remove cwd incidentally' '\n \n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n-\ttest_path_is_dir foo\n+\ttest_path_is_missing foo\n '\n \n-test_expect_failure 'apply does not remove cwd incidentally' '\n+test_expect_success 'rm -f removes cwd incidentally' '\n+\ttest_when_finished \"git reset --hard\" &&\n+\tgit checkout foo/bar/baz &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\tgit rm -f bar/baz.t\n+\t) &&\n+\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\ttest_path_is_missing foo\n+'\n+\n+test_expect_success 'apply removes cwd incidentally' '\n \ttest_when_finished \"git reset --hard\" &&\n \tgit checkout foo/bar/baz &&\n \n@@ -215,10 +226,10 @@ test_expect_failure 'apply does not remove cwd incidentally' '\n \t) &&\n \n \ttest_path_is_missing subdir/file.t &&\n-\ttest_path_is_dir subdir\n+\ttest_path_is_missing subdir\n '\n \n-test_expect_failure 'clean does not remove cwd incidentally' '\n+test_expect_success 'clean does not remove cwd incidentally (cannot match pathspec)' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -227,16 +238,15 @@ test_expect_failure 'clean does not remove cwd incidentally' '\n \t>untracked/random &&\n \t(\n \t\tcd untracked &&\n-\t\tgit clean -fd -e warnings :/ >../warnings &&\n-\t\tgrep \"Refusing to remove current working directory\" ../warnings\n+\t\ttest_must_fail git clean -fd .\n \t) &&\n \n-\ttest_path_is_missing empty &&\n+\ttest_path_is_dir empty &&\n \ttest_path_is_missing untracked/random &&\n \ttest_path_is_dir untracked\n '\n \n-test_expect_failure 'stash does not remove cwd incidentally' '\n+test_expect_success 'stash removes cwd incidentally' '\n \tgit checkout foo/bar/baz &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n@@ -244,12 +254,11 @@ test_expect_failure 'stash does not remove cwd incidentally' '\n \t>untracked/random &&\n \t(\n \t\tcd untracked &&\n-\t\tgit stash --include-untracked &&\n-\t\tgit status\n+\t\tgit stash --include-untracked\n \t) &&\n \n \ttest_path_is_missing untracked/random &&\n-\ttest_path_is_dir untracked\n+\ttest_path_is_missing untracked\n '\n \n test_done\n"},{"id":"442296","messageId":"211125.86bl28jyil.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"f6129a8ac9c3d052fb7fb508a62d4eedb8d9ed57.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T10:44:29Z","receivedAt":"2021-11-25T10:58:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> Removing the current working directory causes all subsequent git\n> commands run from that directory to get confused and fail with a message\n> about being unable to read the current working directory:\n>\n>     $ git status\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> Non-git commands likely have similar warnings or even errors, e.g.\n>\n>     $ bash -c 'echo hello'\n>     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n>     hello\n\nOkey, but...\n\n> diff --git a/git.c b/git.c\n> index 5ff21be21f3..2c98ab48936 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n>  \n>  \ttrace_command_performance(argv);\n>  \n> +\tstartup_info->original_cwd = xgetcwd();\n> +\n>  \t/*\n>  \t * \"git-xxxx\" is the same as \"git xxxx\", but we obviously:\n>  \t *\n> diff --git a/setup.c b/setup.c\n> index 347d7181ae9..f30657723ea 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -432,6 +432,54 @@ void setup_work_tree(void)\n>  \tinitialized = 1;\n>  }\n>  \n> +static void setup_original_cwd(void)\n> +{\n> +\tstruct strbuf tmp = STRBUF_INIT;\n> +\tconst char *worktree = NULL;\n> +\tint offset = -1;\n> +\n> +\t/*\n> +\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n> +\t * we're an auxiliary tool like git-remote-http or test-tool.\n> +\t */\n> +\tif (!startup_info->original_cwd)\n> +\t\treturn;\n\nThis really doesn't belong in those places, by calling xgetcwd() so\nearly you'll be making commands that don't care about RUN_SETUP at all\ndie. E.g. with this change:\n\n    mkdir x &&\n    cd x &&\n    rm -rf ../x &&\n    git version\n\nWill die.\n\nSo as a follow-up to my comment on this v2's 01/09 I think what's also\nmissing here is something that does that, but instead of \"git version\"\ndoes it for all of the \"while read builtin\" list in t0012-help.sh.\n\nThere's other cans of worms to be had here, the discussion downthread of\nthe not-integrated[1] points to some of that.\n\nI.e. how should the various commands like \"ls-remote\" that can work\nwith/without a repo behave here? That one happens to die before/after,\nbut as noted in that thread that's also a bug.\n\nSo anything that adds more really early dying in this area should also\nthink about the greater goals of what we're doing with RUN_SETUP &\nrelated flags. I.e. does the direction make sense?\n\nIf this is moved to some soft recording of the getcwd() (and maybe the\n$PWD, as in my WIP change?) shouldn't this go into common-main.c? With\ngit.c's cmd_main() we're excluding the test helpers and things like\ngit-daemon. Is that intentional?\n\nI'd also think we'd want to do this much earlier if e.g.  thing like the\ntrace2 setup wanted to call the remove_directory() call.\n\nPer what Glen & mentioned I'm still not sure if I'm on board with that\nidea at all, just running with the ball you put in play :) I.e. if we're\nmodifying all callers, let's make sure we catch all callers.\n\nPerhaps a better approach is to intercept chdir() instead? And anything\nthat calls chdir() sets some GIT_* variable so we'll pass \"here's our\noriginal cwd\" down to sub-processes, like we do with \"git -c\" for\nconfig.\n\nThat would presumably save you the effort of in-advance whitelisting\neverything like \"git version\", we can just move to doing this lazily. If\nyou're a command that does a RUN_SETUP or otherwise needs chdir() we'll\nrecord the original getcwd(), otherwise...\n\n1. https://lore.kernel.org/git/patch-1.1-fc26c46d39-20210722T140648Z-avarab@gmail.com/\n"},{"id":"442297","messageId":"211125.867dcwjyeu.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"e74975e83cc7a11b8f0378d59a8c2c4a97d3aa50.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 3/9] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T10:56:18Z","receivedAt":"2021-11-25T11:00:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> +\t/* ERROR_CWD_IN_THE_WAY */\n> +\t\"Refusing to remove '%s' since it is the current working directory.\",\n> +\n>  \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n>  \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n>  \n> @@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n>  \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n>  \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n>  \n> +\tmsgs[ERROR_CWD_IN_THE_WAY] =\n> +\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n> +\n\nWe end up capitalizing the first letter here, which isn't our usual\nstyle, but I see we do it for all of unpack_plumbing_errors already, and\nsome related things like the error setup.c would emit.\n\nStill, perhaps it's better to not follow that convention for new\nmessages?\n"},{"id":"442298","messageId":"211125.8635nkjyby.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"edec0894ca2a01c7afbc70590efd5ee8a0d1abde.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 7/9] stash: do not attempt to remove startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T10:58:04Z","receivedAt":"2021-11-25T11:02:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  builtin/stash.c      | 6 +++++-\n>  t/t2501-cwd-empty.sh | 2 +-\n>  2 files changed, 6 insertions(+), 2 deletions(-)\n>\n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index a0ccc8654df..50b4875980c 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -1485,8 +1485,12 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n>  \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n>  \n>  \t\t\tcp.git_cmd = 1;\n> +\t\t\tif (startup_info->original_cwd &&\n> +\t\t\t    *startup_info->original_cwd &&\n> +\t\t\t    !is_absolute_path(startup_info->original_cwd))\n> +\t\t\t\tcp.dir = startup_info->original_cwd;\n>  \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n> -\t\t\t\t     \"--quiet\", \"-d\", NULL);\n> +\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n\nI saw you used :/ earlier in 01, and I commented that maybe we can use\n\".\".\n\nRight now I can't remember the difference. I think for tired reviewer\neyes it would really help to amend the commit message to call out what\ndesired behavior change we're getting to by adding this pathspec.\n"},{"id":"442299","messageId":"211125.86y25ciisr.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BH_NfkzX5XkhPPpX1_bsR9kWGsBBdcUwikDWsA8jc6KJA@mail.gmail.com","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T11:12:04Z","receivedAt":"2021-11-25T11:27:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Nov 24 2021, Elijah Newren wrote:\n\n> On Wed, Nov 24, 2021 at 6:18 AM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> I fleshened this out a bit in this WIP change:\n>> https://github.com/avar/git/tree/avar/setup-handle-gone-directory +\n>> commit:\n>> https://github.com/avar/git/commit/97968518909eef88edba44973b7885d154b7a273\n>\n> Sweet!\n>\n>> As noted there there's some caveats, but so far nothing I spotted that\n>> can't be overcome. It's particularly painful to test it because of an\n>> implementation detail of our test suite, the bin-wrappers are\n>> shellscripts, and the very first thing they do is reset $PWD (none of\n>> which happens if you run the real \"git\" binary).\n>>\n>> That's b.t.w. the \"shell-init\" error you noted in\n>> https://lore.kernel.org/git/CABPp-BEp3OL7F2J_LzqtC-x-8pBUPO8ZR1fTx_6XbqZeOH1kRw@mail.gmail.com/,\n>> it's from the bin-wrapper.\n>\n> To be a bit more precise, the error/warning is from bash rather than\n> the bin-wrappers specifically; for example, you can also trigger the\n> same shell-init message with a simple `bash -c 'echo hello'` when your\n> cwd has been deleted.\n\n*nod*\n\n>> I really wish we didn't have the bin-wrappers...\n>\n> I'm curious: is this issue the only reason, or am I reading between\n> the lines correctly that you have additional reasons for that?\n\nI think it sucks in general :)\n\nThis is a much bigger change than what's needed for this topic or the\nproposed setup.c change I've got, but what we should be doing in general\nwith bin-wrapper is to not have them at all under RUNTIME_PREFIX=Y\n(which we can make the default), and simply have a built/ directory or\nsomething, the structure of that directory will then 1=1 map to all the\nrelative paths you'd get if you did a \"make install\".\n\nCurrently in a built checkout you can do:\n\n    ./git version\n\nBut not:\n\n    # need to add --exec-path=$PWD\n    ./git clone <http url>\n\nOr:\n\n    ./git send-email ...\n\nWhich all comes down to sillyness in how we try to discover these paths,\nthe *.mo files etc., there's also Perl-specific caveats in there.\n\nWe'd still need something like a shellscript wrapper for the case of\nGIT_DEBUGGER, but we could just move that to being a compile-time flag,\ni.e. we'd swap out the \"git\" binary for a shellscript that chain-loaded\nto the \"real\" binary.\n\nIt probably matters a bit for e.g. test suite performance, currently we\nspawn an extra shell for every single \"git\" invocation, but I mostly\ncare about it for correctness, e.g. in this case of $PWD munging, and\nbeing able to have running a built binary from a checkout Just Work\nwithout the wrapper.\n"},{"id":"442302","messageId":"211125.86tug0ihj4.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BH-MtGLAk3vkFCXr8sB3KmMEp98J1wpjO=v6MbXH4tPHg@mail.gmail.com","subject":"Re: [PATCH 1/8] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T10:04:46Z","receivedAt":"2021-11-25T11:51:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Nov 22 2021, Elijah Newren wrote:\n\n> On Mon, Nov 22, 2021 at 6:27 PM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> On Mon, Nov 22 2021, Elijah Newren wrote:\n>>\n>> > On Sun, Nov 21, 2021 at 9:59 AM Ævar Arnfjörð Bjarmason\n>> > <avarab@gmail.com> wrote:\n>> >>\n>> >> On Sun, Nov 21 2021, Elijah Newren via GitGitGadget wrote:\n>> >>\n>> >> > From: Elijah Newren <newren@gmail.com>\n>> >>\n>> >> > +test_expect_failure 'checkout fails if cwd needs to be removed' '\n>> >> > +     git checkout foo/bar/baz &&\n>> >> > +     test_when_finished \"git clean -fdx\" &&\n>> >> > +\n>> >> > +     mkdir dirORfile &&\n>> >> > +     (\n>> >> > +             cd dirORfile &&\n>> >> > +\n>> >> > +             test_must_fail git checkout fd_conflict 2>../error &&\n>> >> > +             grep \"Refusing to remove the current working directory\" ../error\n>> >> > +     ) &&\n>> >> > +\n>> >> > +     test_path_is_dir dirORfile\n>> >>\n>> >>\n>> >> I'd find this & the rest of this series much easier to understand if we\n>> >> started out by positively asserting the current behavior here, and\n>> >> didn't test_cmp/grep for erro r messages that don't exist anymore.\n>> >\n>> > Yeah, this is my fault for my bad commit message.  I stated I was\n>> > adding tests checking for the problems of interest, making it sound\n>> > like I was testing existing behavior, but I should have stated I was\n>> > adding tests with the behavior we'd prefer to have (i.e. basically a\n>> > test-driven-development) setup.\n>> >\n>> > Also, there really wouldn't need to be so many tests for describing\n>> > the existing behavior.  It's basically just `git\n>> > $OPERATION_THAT_REMOVES_CWD_AS_SIDE_EFFECT` followed by nearly any\n>> > other git command will cause the second and later commands to fail\n>> > with:\n>> >\n>> > ```\n>> > shell-init: error retrieving current directory: getcwd: cannot access\n>> > parent directories: No such file or directory\n>> > fatal: Unable to read current working directory: No such file or directory\n>> > ```\n>> >\n>> > However, we do need a lot of tests for corrected behavior, because\n>> > there are so many different codepaths we can follow which will lead to\n>> > deletion of the current working directory.\n>>\n>> Currently if I do e.g.:\n>>\n>>     git checkout master\n>>     git clean -dxf\n>>     cd perl\n>>     git checkout v0.99\n>>     cd ../\n>>     git clean -dxfn\n>>\n>> Nothing breaks and I don't end up with an empty perl/ to remove. With\n>> these patches we'd either die on the \"checkout\" (I think) keep the\n>> \"perl\" and have an empty perl/ to report in the \"git clean -dxfn\" at the\n>> end (I'm not sure which, I forgot and haven't re-read this series just\n>> now).\n>\n> You'd have an empty perl/ left after the checkout, which would be\n> cleaned up by your final git clean command.\n\nFWIW that's with a -n, just showing that we've got things left.\n\nI.e. just a demo of the new state, in any case not really applicable\nsince the end-state behavior is that we die...\n\n>> I think changing it anyway might be justifiable, but changing the\n>> behavior of things like that tickles my spidey sense a bit. I.e. I can\n>> see people having written scripts like that which would break (it's\n>> often easier to cd around after globbing than staying at the top-level,\n>> then jump back).\n>\n> I disagree this would break any user scripts.  If people expect a 'git\n> checkout' or 'git rebase' to always work, their script is _already_\n> broken.\n\nBroken as in they'll upgrade git and a previously working script or\ncronjob stops working.\n\nMaybe it's too obscure to worry about, but saying nothing broke that\nwasn't working before seems to be moving goalposts to an odd place.\n\nAnyway, I see from\nhttps://lore.kernel.org/git/CABPp-BETpWU9Rkd6pcxh6+gav2QtYnu_5V8ji_1_3kMnVswp1Q@mail.gmail.com/\nthat you're not really saying that & that we've partially got a split\nthread here. Just replying here...\n\n> The presence of any untracked files within the directory\n> already results in a hard error -- we refuse to remove non-empty\n> directories (unless all files are tracked and unmodified).  This rule\n> deserves a clarification: treat the current working directory as\n> non-empty since the parent process is likely still parked there..\n\nI'm not going to make this my hill to die on, and honestly wouldn't care\nthat much if this were changed, aside from wondering how much is really\nthe x-y problem of setup.c.\n\nBut just in the interest of clarity, what I'm talking about is that git\nis a *nix tool, and usually just follows *nix FS semantics.\n\nI think we should have a strong bias towards continuing until the OS\ntell us to stop for any FS operations. Rather than aborting early in\nanticipation of an eventual error, particularly when as I've shown that\neventual error is fixable.\n\n> Further, our own commands are broken/misbehaving due to us not\n> erroring out; see e.g.\n> https://lore.kernel.org/git/xmqqv93n7q1v.fsf@gitster.g/ and its\n> grandparent.  User scripts likely have lurking problems too.\n\n*Nod*. Not to go on about it, but I think those issues are pointing out\nthings that would be fixed by the the more narrow WIP patch I've got of\n\"let's just make setup.c not suck in that case\" :)\n\n>> So I wonder (especially with Glen's comment in\n>> <20211123003958.3978-1-chooglen@google.com>) if this is being done at\n>> the right API level.\n>\n> Glen's comment was interesting, but provided no specifics about how\n> the changes I made could cause any possible harm in his case.\n> Further, the fact that others are adding extra places doing cleanup\n> sound like additional codepaths that should be protected for the exact\n> same reasons.  I think we absolutely want my changes affecting his new\n> codepaths.\n\nAgain, \"not my hill\" etc., but I think the harm is pretty much that\nevery time we start adding any sort of unexpected special-sauce on top\nof doing stuff until we get a syscall error we're subverting the default\nexpectations of users.\n\nI think about this as being in the same category as changing \"checkout\",\n\"rm\" etc. to error out if we detect the target file is open (Windows),\nor refusing to remove an executable that we detect as being in use\n(AIX).\n\nIf you're not on those platforms having a random tool you're scripting\nimposing strictures outside of what the OS would do is just another\nthing to explain to users. Let's just try and run into the OS error, or\nfix our handling of the subsequent error (if any).\n\n>> E.g. maybe it would be better for some commands to\n>> ease into this with an advise() or warning() and not a die() or error(),\n>> or have the die() be in the likes of \"git switch\" but not \"reset\n>> --hard\".\n>\n> The commands that don't need to remove the current working directory\n> but just were as a convenience, no longer do and continue on just\n> fine.  Commands that need to remove the current working directory in\n> order to place a file there will error out, just as they would have\n> when trying to remove a directory with untracked files.  I see no need\n> to ease into anything here.\n\nWhatever direction we decide on here, I really don't think it makes\nsense to think about tracked and modified local content the same as\nuntracked and unmodified content.\n\nFor \"tracked and modified\" we clearly \"own\" it, and erroring out is what\neveryone would expect us to do. We don't want git to shred user data.\n\nFor \"untracked and unmodified\" (the part after \"and\" being redundant\n:-)) we've got the \"precious\" edge-case, i.e. we could shred something\nthe user cares about (which you've also been working on & I very much\napplaud).\n\nBut I think by any sane definition we'd still classify such content as\nless important (since in those cases of shredding it's matching a\n.gitignore).\n\nAnd surely *way lower* in that foodchain is \"untracked and\nirrelevant\". I.e. it's conceivable that someone is making meaningful use\nof an empty directory, but it's very unlikely, and in any case your\nargument for this change isn't that the directory is important per-se,\nbut that \n\nYou note in\nhttps://lore.kernel.org/git/CABPp-BETpWU9Rkd6pcxh6+gav2QtYnu_5V8ji_1_3kMnVswp1Q@mail.gmail.com/\nthat for \"rm -r\" we already left the directory behind for a \"dir\" that\nhad partially tracked/untracked content, that's correct. I'm referring\nto the case where \"x\" is entirely tracked.\n\n>> Or maybe not, just food for thought...\n>\n> You may also be interested in reading more of the other thread I\n> linked to from my cover letter; all these cases were discussed in good\n> detail over there.  For example, look at\n> https://lore.kernel.org/git/CABPp-BFmU+RaAjq4_0-PSfRgH1Jc63nN0fMuDWk2+iDbdz7CCA@mail.gmail.com/.\n> Peff's previous suggestion was to just make the commands error out if\n> they'd normally remove the current working directory and require the\n> user to run from a different directory instead.  My version lightened\n> that requirement so it only errors out if the current working\n> directory needed to be removed in order to place something else there\n> (and if nothing else was needed to be placed there, then just leaving\n> the directory around).\n\nI've read through those, and might have missed something, but it seems\nto all really be discussing the shortcomings of setup.c's handling of\nfailing getcwd() by proxy.\n\nThat and references to other 3rd party commands potentially being\nequally confused.\n\nIn summary I think we should aim more towards:\n\n    $ grep -m 1 Git ../README.md \n    Git - fast, scalable, distributed revision control system\n\nThan:\n\n    $ git grep --no-index -m 1 . ../README.md \n    fatal: Unable to read current working directory: No such file or directory\n\nWhich fails due to the setup.c issue I've noted, but also because we\nstupidly really translate that to the equivalent of a (in an x/ that\ndoesn't exist anymore):\n\n    $ grep -m 1 Git ../x/../README.md \n    grep: ../x/../README.md: No such file or directory\n\n\n"},{"id":"442306","messageId":"f63ed861-1109-d892-597e-25949b08b8cf@iee.email","threadId":"56952","inReplyTo":"xmqqk0gx1gmz.fsf@gitster.g","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2021-11-25T12:54:24Z","receivedAt":"2021-11-25T12:56:31Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 24/11/2021 19:46, Junio C Hamano wrote:\n> Philip Oakley <philipoakley@iee.email> writes:\n>\n>> On 24/11/2021 11:14, Ævar Arnfjörð Bjarmason wrote:\n>>> I'm not concerned that you didn't research this change well enough, I\n>>> just find it a bit iffy to introduce semantics in git around FS\n>>> operations that don't conform with that of POSIX & the underlying OS. My\n>>> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n>>> I'm in, I'd expect git to do the same.\n>> Isn't this the same, conceptually, as trying to remove the root\n>> directory, but from a Git perspective?\n>>\n>> i.e. Something along the lines of\n>> https://superuser.com/questions/542978/is-it-possible-to-remove-the-root-directory\n>> (their answer is 'no' without a special option, default since 2006)\n>>\n>> If I read the arguments correctly, Elijah is saying that Git shouldn't\n>> delete it's own root (cwd) directory, and that it is already implicit\n>> within the current Git code.\n> I do not think it is about protecting \"root\"; the series wants\n>\n>     cd t/ && git rm -r ../t\n>\n> to leave an empty directory at 't/', because \"git rm\" was started in\n> that directory.\nMy point was about where the conceptual 'root' (for Git and it's rm\ncommand) was deemed to be.\n\nFor instance, can/should we be able to elevate ourselves into a super\nproject for the deletion? I did notice that a regular `cd / && cd\n../../` will happily recycle itself at `/`, rather than bugging out.\n\nWhichever way is decided (cwd, GIT_WORK_TREE, or higher), ensuring that\nthe documentation is plain and clear , and not just the code, is\nimportant for future readers, to help avoid future confusions.\n\nPhilip\n"},{"id":"442309","messageId":"211125.86lf1cibkf.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"f63ed861-1109-d892-597e-25949b08b8cf@iee.email","subject":"Re: [PATCH 8/8] dir: avoid removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-25T13:51:55Z","receivedAt":"2021-11-25T14:00:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Philip Oakley wrote:\n\n> On 24/11/2021 19:46, Junio C Hamano wrote:\n>> Philip Oakley <philipoakley@iee.email> writes:\n>>\n>>> On 24/11/2021 11:14, Ævar Arnfjörð Bjarmason wrote:\n>>>> I'm not concerned that you didn't research this change well enough, I\n>>>> just find it a bit iffy to introduce semantics in git around FS\n>>>> operations that don't conform with that of POSIX & the underlying OS. My\n>>>> *nix system happily accepts an \"rm -rf\" or an \"rmdir\" of the directory\n>>>> I'm in, I'd expect git to do the same.\n>>> Isn't this the same, conceptually, as trying to remove the root\n>>> directory, but from a Git perspective?\n>>>\n>>> i.e. Something along the lines of\n>>> https://superuser.com/questions/542978/is-it-possible-to-remove-the-root-directory\n>>> (their answer is 'no' without a special option, default since 2006)\n>>>\n>>> If I read the arguments correctly, Elijah is saying that Git shouldn't\n>>> delete it's own root (cwd) directory, and that it is already implicit\n>>> within the current Git code.\n>> I do not think it is about protecting \"root\"; the series wants\n>>\n>>     cd t/ && git rm -r ../t\n>>\n>> to leave an empty directory at 't/', because \"git rm\" was started in\n>> that directory.\n> My point was about where the conceptual 'root' (for Git and it's rm\n> command) was deemed to be.\n\nMakes sense, but nothing being discussed here has to do with crossing\nrepository boundaries so far.\n\n> For instance, can/should we be able to elevate ourselves into a super\n> project for the deletion? I did notice that a regular `cd / && cd\n> ../../` will happily recycle itself at `/`, rather than bugging out.\n\nThis behavior explicitly standardized in POSIX:\nhttps://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html:\n\n    The special filename dot shall refer to the directory\n    specified byits predecessor. The special filename dot-dot\n    shall refer to the parent directory of its predecessor\n    directory. As a special case, in the root directory, dot-dot\n    may refer to the root directory itself.c\n\nThe same goes for \"foo\", \"./foo\", \"./././././foo\" etc.\n\n> Whichever way is decided (cwd, GIT_WORK_TREE, or higher), ensuring that\n> the documentation is plain and clear , and not just the code, is\n> important for future readers, to help avoid future confusions.\n\nYes, offhand I don't know where we canonically document our behavior of\nnot crossing repo boundaries, or rather not going \"up\".\n"},{"id":"442343","messageId":"xmqqo8678l3x.fsf@gitster.g","threadId":"56952","inReplyTo":"f6129a8ac9c3d052fb7fb508a62d4eedb8d9ed57.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-26T06:52:18Z","receivedAt":"2021-11-26T06:54:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/git.c b/git.c\n> index 5ff21be21f3..2c98ab48936 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n>  \n>  \ttrace_command_performance(argv);\n>  \n> +\tstartup_info->original_cwd = xgetcwd();\n\nWe assume that, unless we are an auxiliary tool like git-remote-http,\nwe should always have cwd?\n\n> diff --git a/setup.c b/setup.c\n> index 347d7181ae9..f30657723ea 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -432,6 +432,54 @@ void setup_work_tree(void)\n>  \tinitialized = 1;\n>  }\n>  \n> +static void setup_original_cwd(void)\n> +{\n> +\tstruct strbuf tmp = STRBUF_INIT;\n> +\tconst char *worktree = NULL;\n> +\tint offset = -1;\n> +\n> +\t/*\n> +\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n\ns/wass/was/;\n\n> +\t * we're an auxiliary tool like git-remote-http or test-tool.\n> +\t */\n> +\tif (!startup_info->original_cwd)\n> +\t\treturn;\n> +\n> +\t/*\n> +\t * startup_info->original_cwd points to the current working\n> +\t * directory we inherited from our parent process, which is a\n> +\t * directory we want to avoid incidentally removing.\n> +\t *\n> +\t * For convience, we would like to have the path relative to the\n> +\t * worktree instead of an absolute path.\n> +\t *\n> +\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n> +\t * but differs in two ways:\n> +\t *   - prefix has a trailing '/'\n> +\t *   - if the user passes '-C' to git, that modifies the prefix but\n> +\t *     not startup_info->original_cwd.\n> +\t */\n> +\n> +\t/* Normalize the directory */\n> +\tstrbuf_realpath(&tmp, startup_info->original_cwd, 1);\n> +\tfree((char*)startup_info->original_cwd);\n> +\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n> +\n> +\t/* Find out if this is in the worktree */\n> +\tworktree = get_git_work_tree();\n> +\tif (worktree)\n> +\t\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n> +\tif (offset >= 0) {\n> +\t\t/*\n> +\t\t * original_cwd was inside worktree; precompose it just as\n> +\t\t * we do prefix so that built up paths will match\n> +\t\t */\n> +\t\tstartup_info->original_cwd = \\\n> +\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n> +\t\t\t\t\t\t    + offset);\n> +\t}\n\nI wonder if we want to clear the .original_cwd member, so that the\n\"cwd protection\" do not have to worry about anything at all when we\nare in a bare repository.\n\n> +}\n> +\n>  static int read_worktree_config(const char *var, const char *value, void *vdata)\n>  {\n>  \tstruct repository_format *data = vdata;\n> @@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>  \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n>  \t}\n>  \n> +\tsetup_original_cwd();\n>  \n>  \tstrbuf_release(&dir);\n>  \tstrbuf_release(&gitdir);\n"},{"id":"442368","messageId":"CABPp-BFN9y_nX1pEYAOyJKBhTXHWOGxb66bBs6=eJABKophSBg@mail.gmail.com","threadId":"56952","inReplyTo":"211125.86bl28jyil.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-26T17:55:56Z","receivedAt":"2021-11-26T18:11:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 25, 2021 at 2:55 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > Removing the current working directory causes all subsequent git\n> > commands run from that directory to get confused and fail with a message\n> > about being unable to read the current working directory:\n> >\n> >     $ git status\n> >     fatal: Unable to read current working directory: No such file or directory\n> >\n> > Non-git commands likely have similar warnings or even errors, e.g.\n> >\n> >     $ bash -c 'echo hello'\n> >     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n> >     hello\n>\n> Okey, but...\n>\n> > diff --git a/git.c b/git.c\n> > index 5ff21be21f3..2c98ab48936 100644\n> > --- a/git.c\n> > +++ b/git.c\n> > @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n> >\n> >       trace_command_performance(argv);\n> >\n> > +     startup_info->original_cwd = xgetcwd();\n> > +\n> >       /*\n> >        * \"git-xxxx\" is the same as \"git xxxx\", but we obviously:\n> >        *\n> > diff --git a/setup.c b/setup.c\n> > index 347d7181ae9..f30657723ea 100644\n> > --- a/setup.c\n> > +++ b/setup.c\n> > @@ -432,6 +432,54 @@ void setup_work_tree(void)\n> >       initialized = 1;\n> >  }\n> >\n> > +static void setup_original_cwd(void)\n> > +{\n> > +     struct strbuf tmp = STRBUF_INIT;\n> > +     const char *worktree = NULL;\n> > +     int offset = -1;\n> > +\n> > +     /*\n> > +      * startup_info->original_cwd wass set early on in cmd_main(), unless\n> > +      * we're an auxiliary tool like git-remote-http or test-tool.\n> > +      */\n> > +     if (!startup_info->original_cwd)\n> > +             return;\n>\n> This really doesn't belong in those places, by calling xgetcwd() so\n> early you'll be making commands that don't care about RUN_SETUP at all\n> die. E.g. with this change:\n>\n>     mkdir x &&\n>     cd x &&\n>     rm -rf ../x &&\n>     git version\n>\n> Will die.\n\nWhoops!  Yeah, I should have used strbuf_getcwd() there.  Thanks for\nthe careful reading.\n\n> So as a follow-up to my comment on this v2's 01/09 I think what's also\n> missing here is something that does that, but instead of \"git version\"\n> does it for all of the \"while read builtin\" list in t0012-help.sh.\n\nIf I had to do some kind of special casing, or if I did potentially\ncall a path that could die() in the final version, then yes I'd need\nto test all of these.  But by using strbuf_getcwd(), I remove the\npossibility of die()'ing, so I think testing one of these is good\nenough.\n\n> There's other cans of worms to be had here, the discussion downthread of\n> the not-integrated[1] points to some of that.\n>\n> I.e. how should the various commands like \"ls-remote\" that can work\n> with/without a repo behave here? That one happens to die before/after,\n> but as noted in that thread that's also a bug.\n>\n> So anything that adds more really early dying in this area should also\n> think about the greater goals of what we're doing with RUN_SETUP &\n> related flags. I.e. does the direction make sense?\n>\n> If this is moved to some soft recording of the getcwd() (and maybe the\n> $PWD, as in my WIP change?) shouldn't this go into common-main.c? With\n> git.c's cmd_main() we're excluding the test helpers and things like\n> git-daemon. Is that intentional?\n\nI didn't think e.g. git-remote-http, git-daemon, etc. mattered.\ntest-tool is supposed to only be used in the testsuite to my\nknowledge.  But fair point, I'll move it to common-main.c.  And, as\nper Junio's suggestion in this thread, I'll make it only affect\nworktrees.\n\n> I'd also think we'd want to do this much earlier if e.g.  thing like the\n> trace2 setup wanted to call the remove_directory() call.\n\nNah, I'll just put the original_cwd in a temporary, and then if setup\nis never called, it won't affect anything else.  And setup will only\nmake it affect the worktree (and thus won't affect bare repos, nor\naffect paths outside the worktree if the user is running from outside\nthe worktree).\n\n> Per what Glen & mentioned I'm still not sure if I'm on board with that\n> idea at all, just running with the ball you put in play :) I.e. if we're\n> modifying all callers, let's make sure we catch all callers.\n>\n> Perhaps a better approach is to intercept chdir() instead? And anything\n> that calls chdir() sets some GIT_* variable so we'll pass \"here's our\n> original cwd\" down to sub-processes, like we do with \"git -c\" for\n> config.\n>\n> That would presumably save you the effort of in-advance whitelisting\n> everything like \"git version\", we can just move to doing this lazily. If\n> you're a command that does a RUN_SETUP or otherwise needs chdir() we'll\n> record the original getcwd(), otherwise...\n\nThere is a chdir_notify(), but of course it isn't called by the\ncodepath that handles the -C argument.  I believe the above solution\nof a temporary handles things just fine, though.\n\nThanks for the thoughtful and careful review!\n"},{"id":"442369","messageId":"CABPp-BG8narMYNeaBHQfhcOFJ4hHJM+HGoy7S4hyOA-7PbOEPA@mail.gmail.com","threadId":"56952","inReplyTo":"xmqqo8678l3x.fsf@gitster.g","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-26T18:01:41Z","receivedAt":"2021-11-26T18:22:39Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 25, 2021 at 10:52 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > diff --git a/git.c b/git.c\n> > index 5ff21be21f3..2c98ab48936 100644\n> > --- a/git.c\n> > +++ b/git.c\n> > @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n> >\n> >       trace_command_performance(argv);\n> >\n> > +     startup_info->original_cwd = xgetcwd();\n>\n> We assume that, unless we are an auxiliary tool like git-remote-http,\n> we should always have cwd?\n\nYeah, Ævar caught this mistake too; I should have used\nstrbuf_getcwd().  Will fix.\n\n> > diff --git a/setup.c b/setup.c\n> > index 347d7181ae9..f30657723ea 100644\n> > --- a/setup.c\n> > +++ b/setup.c\n> > @@ -432,6 +432,54 @@ void setup_work_tree(void)\n> >       initialized = 1;\n> >  }\n> >\n> > +static void setup_original_cwd(void)\n> > +{\n> > +     struct strbuf tmp = STRBUF_INIT;\n> > +     const char *worktree = NULL;\n> > +     int offset = -1;\n> > +\n> > +     /*\n> > +      * startup_info->original_cwd wass set early on in cmd_main(), unless\n>\n> s/wass/was/;\n\nThanks.\n\n> > +      * we're an auxiliary tool like git-remote-http or test-tool.\n> > +      */\n> > +     if (!startup_info->original_cwd)\n> > +             return;\n> > +\n> > +     /*\n> > +      * startup_info->original_cwd points to the current working\n> > +      * directory we inherited from our parent process, which is a\n> > +      * directory we want to avoid incidentally removing.\n> > +      *\n> > +      * For convience, we would like to have the path relative to the\n> > +      * worktree instead of an absolute path.\n> > +      *\n> > +      * Yes, startup_info->original_cwd is usually the same as 'prefix',\n> > +      * but differs in two ways:\n> > +      *   - prefix has a trailing '/'\n> > +      *   - if the user passes '-C' to git, that modifies the prefix but\n> > +      *     not startup_info->original_cwd.\n> > +      */\n> > +\n> > +     /* Normalize the directory */\n> > +     strbuf_realpath(&tmp, startup_info->original_cwd, 1);\n> > +     free((char*)startup_info->original_cwd);\n> > +     startup_info->original_cwd = strbuf_detach(&tmp, NULL);\n> > +\n> > +     /* Find out if this is in the worktree */\n> > +     worktree = get_git_work_tree();\n> > +     if (worktree)\n> > +             offset = dir_inside_of(startup_info->original_cwd, worktree);\n> > +     if (offset >= 0) {\n> > +             /*\n> > +              * original_cwd was inside worktree; precompose it just as\n> > +              * we do prefix so that built up paths will match\n> > +              */\n> > +             startup_info->original_cwd = \\\n> > +                     precompose_string_if_needed(startup_info->original_cwd\n> > +                                                 + offset);\n> > +     }\n>\n> I wonder if we want to clear the .original_cwd member, so that the\n> \"cwd protection\" do not have to worry about anything at all when we\n> are in a bare repository.\n\nYeah, I wondered about this a bit and whether it'd even matter either\nway if the original_cwd was something outside the working tree.  I can\nobviously come up with scenarios, but can't see protecting or not\nprotecting such directories as mattering; if the user is in a\ndirectory where git internals are stored and deleted, well, they're\nalready off-roading.\n\nBut, since you bring it up, I might as well scope this to just\nprotecting a directory within the working tree.  Perhaps that'd make\nÆvar more comfortable with the change too.  I'll do that.\n"},{"id":"442370","messageId":"CABPp-BEMY63=SF17UsY=VePADNoa186UNq0LHXrxRwV8Q_CYWQ@mail.gmail.com","threadId":"56952","inReplyTo":"211125.8635nkjyby.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 7/9] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-26T18:04:39Z","receivedAt":"2021-11-26T18:28:44Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 25, 2021 at 2:58 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  builtin/stash.c      | 6 +++++-\n> >  t/t2501-cwd-empty.sh | 2 +-\n> >  2 files changed, 6 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/builtin/stash.c b/builtin/stash.c\n> > index a0ccc8654df..50b4875980c 100644\n> > --- a/builtin/stash.c\n> > +++ b/builtin/stash.c\n> > @@ -1485,8 +1485,12 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n> >                       struct child_process cp = CHILD_PROCESS_INIT;\n> >\n> >                       cp.git_cmd = 1;\n> > +                     if (startup_info->original_cwd &&\n> > +                         *startup_info->original_cwd &&\n> > +                         !is_absolute_path(startup_info->original_cwd))\n> > +                             cp.dir = startup_info->original_cwd;\n> >                       strvec_pushl(&cp.args, \"clean\", \"--force\",\n> > -                                  \"--quiet\", \"-d\", NULL);\n> > +                                  \"--quiet\", \"-d\", \":/\", NULL);\n>\n> I saw you used :/ earlier in 01, and I commented that maybe we can use\n> \".\".\n\nI tweaked that other one to avoid :/ a different way.\n\n> Right now I can't remember the difference. I think for tired reviewer\n> eyes it would really help to amend the commit message to call out what\n> desired behavior change we're getting to by adding this pathspec.\n\nYeah, I want the ':/' to be explicit that it should clean from the\ntoplevel of the repository despite starting in a subdirectory; I'll\namend the commit message.\n"},{"id":"442372","messageId":"CABPp-BH8iE=78Ni0yNaotmvtQps1LnAXfg8X1veO44Q4cf6yMA@mail.gmail.com","threadId":"56952","inReplyTo":"211125.867dcwjyeu.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v2 3/9] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-26T18:06:55Z","receivedAt":"2021-11-26T18:32:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 25, 2021 at 2:57 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Nov 25 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > +     /* ERROR_CWD_IN_THE_WAY */\n> > +     \"Refusing to remove '%s' since it is the current working directory.\",\n> > +\n> >       /* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n> >       \"Untracked working tree file '%s' would be overwritten by merge.\",\n> >\n> > @@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n> >       msgs[ERROR_NOT_UPTODATE_DIR] =\n> >               _(\"Updating the following directories would lose untracked files in them:\\n%s\");\n> >\n> > +     msgs[ERROR_CWD_IN_THE_WAY] =\n> > +             _(\"Refusing to remove the current working directory:\\n%s\");\n> > +\n>\n> We end up capitalizing the first letter here, which isn't our usual\n> style, but I see we do it for all of unpack_plumbing_errors already, and\n> some related things like the error setup.c would emit.\n>\n> Still, perhaps it's better to not follow that convention for new\n> messages?\n\nIf someone else wants to submit a patch afterwards to clean up all\nthese error messages, that'd be fine, but I think being internally\ninconsistent is more jarring than not following the global rule.  I'll\nleave it as is, and let someone else do the wider cleanup.\n"},{"id":"442375","messageId":"211126.868rxaioem.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"211125.86y25ciisr.gmgdl@evledraar.gmail.com","subject":"The overhead of bin-wrappers/ (was: [PATCH 8/8] dir: avoid removing the current working directory)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-26T21:40:00Z","receivedAt":"2021-11-26T21:45:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Nov 25 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> On Wed, Nov 24 2021, Elijah Newren wrote:\n>\n>> On Wed, Nov 24, 2021 at 6:18 AM Ævar Arnfjörð Bjarmason\n>> <avarab@gmail.com> wrote:\n> [...]\n>>> I really wish we didn't have the bin-wrappers...\n>>\n>> I'm curious: is this issue the only reason, or am I reading between\n>> the lines correctly that you have additional reasons for that?\n>\n> I think it sucks in general :)\n>\n> This is a much bigger change than what's needed for this topic or the\n> proposed setup.c change I've got, but what we should be doing in general\n> with bin-wrapper is to not have them at all under RUNTIME_PREFIX=Y\n> (which we can make the default), and simply have a built/ directory or\n> something, the structure of that directory will then 1=1 map to all the\n> relative paths you'd get if you did a \"make install\".\n>\n> Currently in a built checkout you can do:\n>\n>     ./git version\n>\n> But not:\n>\n>     # need to add --exec-path=$PWD\n>     ./git clone <http url>\n>\n> Or:\n>\n>     ./git send-email ...\n>\n> Which all comes down to sillyness in how we try to discover these paths,\n> the *.mo files etc., there's also Perl-specific caveats in there.\n>\n> We'd still need something like a shellscript wrapper for the case of\n> GIT_DEBUGGER, but we could just move that to being a compile-time flag,\n> i.e. we'd swap out the \"git\" binary for a shellscript that chain-loaded\n> to the \"real\" binary.\n>\n> It probably matters a bit for e.g. test suite performance, currently we\n> spawn an extra shell for every single \"git\" invocation, but I mostly\n> care about it for correctness, e.g. in this case of $PWD munging, and\n> being able to have running a built binary from a checkout Just Work\n> without the wrapper.\n\nIt also adds quite a bit of overhead to the tests, more than I vaguely\nrecalled. It differs by test, but here's one that invokes \"git\" a\nlot\". Trying with GIT_TEST_INSTALLED is a handy way to avoid the\nbin-wrappers:\n    \n    $ hyperfine -L e ,GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin '{e} ./t3070-wildmatch.sh --root=/dev/shm/git'\n    Benchmark 1:  ./t3070-wildmatch.sh --root=/dev/shm/git\n      Time (mean ± σ):      4.385 s ±  0.028 s    [User: 3.730 s, System: 0.824 s]\n      Range (min … max):    4.340 s …  4.430 s    10 runs\n     \n    Benchmark 2: GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin ./t3070-wildmatch.sh --root=/dev/shm/git\n      Time (mean ± σ):      3.737 s ±  0.036 s    [User: 3.240 s, System: 0.656 s]\n      Range (min … max):    3.686 s …  3.796 s    10 runs\n     \n    Summary\n      'GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin ./t3070-wildmatch.sh --root=/dev/shm/git' ran\n        1.17 ± 0.01 times faster than ' ./t3070-wildmatch.sh --root=/dev/shm/git'\n\nSo a ~15-20% speedup, a more common case of a ~10% speedup:\n    \n    $ hyperfine -L e ,GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin '{e} ./t0001-init.sh --root=/dev/shm/git'\n    Benchmark 1:  ./t0001-init.sh --root=/dev/shm/git\n      Time (mean ± σ):     522.8 ms ±  22.8 ms    [User: 438.9 ms, System: 130.2 ms]\n      Range (min … max):   502.8 ms … 581.3 ms    10 runs\n     \n    Benchmark 2: GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin ./t0001-init.sh --root=/dev/shm/git\n      Time (mean ± σ):     473.4 ms ±  12.6 ms    [User: 406.7 ms, System: 117.5 ms]\n      Range (min … max):   461.0 ms … 502.1 ms    10 runs\n     \n    Summary\n      'GIT_TEST_INSTALLED=/home/avar/g/git/installed/bin ./t0001-init.sh --root=/dev/shm/git' ran\n        1.10 ± 0.06 times faster than ' ./t0001-init.sh --root=/dev/shm/git'\n\nAnd that's on a box where process spawning is really fast, and against a\nramdisk.\n"},{"id":"442379","messageId":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v2.git.git.1637829556.gitgitgadget@gmail.com","subject":"[PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:51Z","receivedAt":"2021-11-26T22:45:07Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). Many commands either\nsilently avoid removing directories that are not empty (i.e. those that have\nuntracked or modified files in them)[1], or show an error and abort,\ndepending on which is more appropriate for the command in question. With\nthis series, we augment the reasons to avoid removing directories to include\nnot just has-untracked-or-modified-files, but also to avoid removing the\noriginal_cwd as well.\n\nPeff and Junio provided some good pros/cons, if it helps:\n\n * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\nhttps://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\nChanges since v2:\n\n * the series is now only about the working tree. So if the original cwd is\n   outside the worktree (or we're in a bare repo), then the new code is a\n   no-op.\n * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n   xgetcwd())\n * modified the initial tests to show both expected and desired behavior.\n   subsequent patches fix the tests. One new patch added at the end which\n   simplifies the tests to only check for desired behavior.\n * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n   that is already protected and we don't need secondary protection for it.\n   This simplified some other codepaths so we don't have to check for\n   startup_info->original_cwd == \"\".\n * clarified some commit messages\n\nChanges since v1:\n\n * clarified multiple commit messages\n * renamed the_cwd to startup_info->original_cwd to make it clearer that\n   it's our parent process'es cwd that really matters, which we inherited at\n   program startup. Also pulls it out of the global namespace.\n * Normalize the path for startup_info->original_cwd, and ensure that it's\n   actually the original cwd even if -C is passed to git.\n * small code cleanups suggested by René and Ævar\n * split the final patch, which got the most comments into two, one for each\n   function being modified; significantly extending the first of the two\n   commit messages with a lot of history\n * no longer has a content conflict with so/stash-staged\n * add another value for the flags parameter that remove_dir_recursively()\n   takes so that it can opt into either the old or the new behavior. Use\n   that for the one special corner case I could find where it matters, and\n   add a few tests around it to highlight the utility of the flag.\n\nElijah Newren (11):\n  t2501: add various tests for removing the current working directory\n  setup: introduce startup_info->original_cwd\n  unpack-trees: refuse to remove startup_info->original_cwd\n  unpack-trees: add special cwd handling\n  symlinks: do not include startup_info->original_cwd in dir removal\n  clean: do not attempt to remove startup_info->original_cwd\n  rebase: do not attempt to remove startup_info->original_cwd\n  stash: do not attempt to remove startup_info->original_cwd\n  dir: avoid incidentally removing the original_cwd in remove_path()\n  dir: new flag to remove_dir_recurse() to spare the original_cwd\n  t2501: simplify the tests since we can now assume desired behavior\n\n builtin/clean.c      |  44 +++++--\n builtin/rm.c         |   3 +-\n builtin/stash.c      |   5 +-\n cache.h              |   2 +\n common-main.c        |   4 +\n dir.c                |  15 ++-\n dir.h                |   9 +-\n sequencer.c          |   3 +\n setup.c              |  65 +++++++++++\n symlinks.c           |   8 +-\n t/t2501-cwd-empty.sh | 268 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  30 ++++-\n unpack-trees.h       |   1 +\n 13 files changed, 435 insertions(+), 22 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v3\nPull-Request: https://github.com/git/git/pull/1140\n\nRange-diff vs v2:\n\n  1:  38a120f5c03 !  1:  4b0044656b0 t2501: add various tests for removing the current working directory\n     @@ Metadata\n       ## Commit message ##\n          t2501: add various tests for removing the current working directory\n      \n     -    Numerous commands will remove empty working directories, especially if\n     -    they are in the way of placing needed files.  That is normally fine, but\n     -    removing the current working directory can cause confusion for the user\n     -    when they run subsequent commands.  For example, after one git process\n     -    has removed the current working directory, git status/log/diff will all\n     -    abort with the message:\n     +    Numerous commands will remove directories left empty as a \"convenience\"\n     +    after removing files within them.  That is normally fine, but removing\n     +    the current working directory can be rather inconvenient since it can\n     +    cause confusion for the user when they run subsequent commands.  For\n     +    example, after one git process has removed the current working\n     +    directory, git status/log/diff will all abort with the message:\n      \n              fatal: Unable to read current working directory: No such file or directory\n      \n     +    We also have code paths that, when a file needs to be placed where a\n     +    directory is (due to e.g. checkout, merge, reset, whatever), will check\n     +    if this is okay and error out if not.  These rules include:\n     +      * all tracked files under that directory are intended to be removed by\n     +        the operation\n     +      * none of the tracked files under that directory have uncommitted\n     +        modification\n     +      * there are no untracked files under that directory\n     +    However, if we end up remove the current working directory, we can cause\n     +    user confusion when they run subsequent commands, so we would prefer if\n     +    there was a fourth rule added to this list: avoid removing the current\n     +    working directory.\n     +\n          Since there are several code paths that can result in the current\n          working directory being removed, add several tests of various different\n     -    codepaths that check for the behavior we would instead like to see.\n     -    This include a number of new error messages that we will be adding in\n     -    subsequent commits as we implement the desired checks.\n     +    codepaths.  To make it clearer what the difference between the current\n     +    behavior and the behavior at the end of the series, code both of them\n     +    into the tests and have the appropriate behavior be selected by a flag.\n     +    Subsequent commits will toggle the flag from current to desired\n     +    behavior.\n     +\n     +    Also add a few tests suggested during the review of earlier rounds of\n     +    this patch series.\n      \n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +test_expect_success setup '\n      +\ttest_commit init &&\n     -+\tmkdir subdir &&\n     -+\ttest_commit subdir/file &&\n      +\n      +\tgit branch fd_conflict &&\n      +\n     @@ t/t2501-cwd-empty.sh (new)\n      +\tgit tag reverted &&\n      +\n      +\tgit checkout fd_conflict &&\n     -+\tgit rm subdir/file.t &&\n     ++\tmkdir dirORfile &&\n     ++\ttest_commit dirORfile/foo &&\n     ++\n     ++\tgit rm -r dirORfile &&\n      +\techo not-a-directory >dirORfile &&\n      +\tgit add dirORfile &&\n     -+\tgit commit -m dirORfile\n     ++\tgit commit -m dirORfile &&\n     ++\n     ++\tgit switch -c df_conflict HEAD~1 &&\n     ++\ttest_commit random_file &&\n     ++\n     ++\tgit switch -c undo_fd_conflict fd_conflict &&\n     ++\tgit revert HEAD\n      +'\n      +\n     -+test_expect_failure 'checkout does not clean cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     ++test_incidental_dir_removal () {\n     ++\tworks=$1 &&\n     ++\tshift &&\n     ++\n     ++\ttest_when_finished \"git reset --hard\" &&\n     ++\n     ++\tgit checkout foo/bar/baz^{commit} &&\n      +\ttest_path_is_dir foo/bar &&\n      +\n      +\t(\n      +\t\tcd foo &&\n     -+\t\tgit checkout init &&\n     -+\t\tcd ..\n     ++\t\t\"$@\" &&\n     ++\n     ++\t\t# Although we want pwd & git status to pass, test for existing\n     ++\t\t# rather than desired behavior.\n     ++\t\tif [[ $works == \"success\" ]]; then\n     ++\t\t\tpwd -P &&\n     ++\t\t\tgit status --porcelain\n     ++\t\telse\n     ++\t\t\t! pwd -P &&\n     ++\t\t\ttest_might_fail git status --porcelain\n     ++\t\tfi\n      +\t) &&\n      +\ttest_path_is_missing foo/bar/baz &&\n      +\ttest_path_is_missing foo/bar &&\n     -+\ttest_path_is_dir foo\n     -+'\n      +\n     -+test_expect_failure 'checkout fails if cwd needs to be removed' '\n     -+\tgit checkout foo/bar/baz &&\n     ++\t# Although we want dir to be present, test for existing rather\n     ++\t# than desired behavior.\n     ++\tif [[ $works == \"success\" ]]; then\n     ++\t\ttest_path_is_dir foo\n     ++\telse\n     ++\t\ttest_path_is_missing foo\n     ++\tfi\n     ++}\n     ++\n     ++test_required_dir_removal () {\n     ++\tworks=$1 &&\n     ++\tshift &&\n     ++\n     ++\tgit checkout df_conflict^{commit} &&\n      +\ttest_when_finished \"git clean -fdx\" &&\n      +\n     -+\tmkdir dirORfile &&\n      +\t(\n      +\t\tcd dirORfile &&\n      +\n     -+\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n     -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n     ++\t\t# We'd like for the command to fail (much as it would if there\n     ++\t\t# was an untracked file there), and for pwd & git status to\n     ++\t\t# succeed afterwards.  But test for existing rather than\n     ++\t\t# desired behavior.\n     ++\t\tif [[ $works == \"success\" ]]; then\n     ++\t\t\ttest_must_fail \"$@\" 2>../error &&\n     ++\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     ++\t\t\tpwd -P &&\n     ++\t\t\tgit status --porcelain\n     ++\t\telse\n     ++\t\t\t\"$@\" &&\n     ++\t\t\t! pwd -P &&\n     ++\t\t\ttest_might_fail git status --porcelain\n     ++\t\tfi\n      +\t) &&\n      +\n     -+\ttest_path_is_dir dirORfile\n     ++\t# Although we want dirORfile to be present, test for existing rather\n     ++\t# than desired behavior.\n     ++\tif [[ $works == \"success\" ]]; then\n     ++\t\ttest_path_is_dir dirORfile\n     ++\telse\n     ++\t\ttest_path_is_file dirORfile\n     ++\tfi\n     ++}\n     ++\n     ++test_expect_success 'checkout does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git checkout init\n      +'\n      +\n     -+test_expect_failure 'reset --hard does not clean cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_path_is_dir foo/bar &&\n     ++test_expect_success 'checkout fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git checkout fd_conflict\n     ++'\n      +\n     -+\t(\n     -+\t\tcd foo &&\n     -+\t\tgit reset --hard init &&\n     -+\t\tcd ..\n     -+\t) &&\n     -+\ttest_path_is_missing foo/bar/baz &&\n     -+\ttest_path_is_missing foo/bar &&\n     -+\ttest_path_is_dir foo\n     ++test_expect_success 'reset --hard does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git reset --hard init\n     ++'\n     ++\n     ++test_expect_success 'reset --hard fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git reset --hard fd_conflict\n     ++'\n     ++\n     ++test_expect_success 'merge does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git merge reverted\n      +'\n      +\n     -+test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n     ++# This file uses some simple merges where\n     ++#   Base: 'dirORfile/' exists\n     ++#   Side1: random other file changed\n     ++#   Side2: 'dirORfile/' removed, 'dirORfile' added\n     ++# this should resolve cleanly, but merge-recursive throws merge conflicts\n     ++# because it's dumb.  Add a special test for checking merge-recursive (and\n     ++# merge-ort), then after this just hard require ort for all remaining tests.\n     ++#\n     ++test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n      +\tgit checkout foo/bar/baz &&\n      +\ttest_when_finished \"git clean -fdx\" &&\n      +\n     @@ t/t2501-cwd-empty.sh (new)\n      +\t(\n      +\t\tcd dirORfile &&\n      +\n     -+\t\ttest_must_fail git reset --hard fd_conflict 2>../error &&\n     -+\t\tgrep \"Refusing to remove.*the current working directory\" ../error\n     ++\t\t# We would rather this failed, but we test for existing\n     ++\t\t# rather than desired behavior\n     ++\t\tgit merge fd_conflict 2>../error\n      +\t) &&\n      +\n     -+\ttest_path_is_dir dirORfile\n     ++\t## Here is the behavior we would rather have:\n     ++\t#test_path_is_dir dirORfile &&\n     ++\t#grep \"Refusing to remove the current working directory\" error\n     ++\t## But instead we test for existing behavior\n     ++\ttest_path_is_file dirORfile &&\n     ++\ttest_must_be_empty error\n      +'\n      +\n     -+test_expect_failure 'merge does not remove cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++GIT_TEST_MERGE_ALGORITHM=ort\n      +\n     -+\t(\n     -+\t\tcd subdir &&\n     -+\t\tgit merge fd_conflict\n     -+\t) &&\n     ++test_expect_success 'merge fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git merge fd_conflict\n     ++'\n      +\n     -+\ttest_path_is_missing subdir/file.t &&\n     -+\ttest_path_is_dir subdir\n     ++test_expect_success 'cherry-pick does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git cherry-pick reverted\n      +'\n      +\n     -+test_expect_failure 'merge fails if cwd needs to be removed' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git cherry-pick fd_conflict\n     ++'\n      +\n     -+\tmkdir dirORfile &&\n     -+\t(\n     -+\t\tcd dirORfile &&\n     -+\t\ttest_must_fail git merge fd_conflict 2>../error &&\n     -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n     -+\t) &&\n     ++test_expect_success 'rebase does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git rebase reverted\n     ++'\n      +\n     -+\ttest_path_is_dir dirORfile\n     ++test_expect_success 'rebase fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git rebase fd_conflict\n      +'\n      +\n     -+test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success 'revert does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git revert HEAD\n     ++'\n      +\n     -+\t(\n     -+\t\tcd subdir &&\n     -+\t\tgit cherry-pick fd_conflict\n     -+\t) &&\n     ++test_expect_success 'revert fails if cwd needs to be removed' '\n     ++\ttest_required_dir_removal failure git revert undo_fd_conflict\n     ++'\n      +\n     -+\ttest_path_is_missing subdir/file.t &&\n     -+\ttest_path_is_dir subdir\n     ++test_expect_success 'rm does not clean cwd incidentally' '\n     ++\ttest_incidental_dir_removal failure git rm bar/baz.t\n      +'\n      +\n     -+test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success 'apply does not remove cwd incidentally' '\n     ++\tgit diff HEAD HEAD~1 >patch &&\n     ++\ttest_incidental_dir_removal failure git apply ../patch\n     ++'\n      +\n     -+\tmkdir dirORfile &&\n     -+\t(\n     -+\t\tcd dirORfile &&\n     -+\t\ttest_must_fail git cherry-pick fd_conflict 2>../error &&\n     -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n     -+\t) &&\n     ++test_incidental_untracked_dir_removal () {\n     ++\tworks=$1 &&\n     ++\tshift &&\n      +\n     -+\ttest_path_is_dir dirORfile\n     -+'\n     ++\ttest_when_finished \"git reset --hard\" &&\n      +\n     -+test_expect_failure 'rebase does not remove cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++\tgit checkout foo/bar/baz^{commit} &&\n     ++\tmkdir -p untracked &&\n     ++\tmkdir empty\n     ++\t>untracked/random &&\n      +\n      +\t(\n     -+\t\tcd subdir &&\n     -+\t\tgit rebase foo/bar/baz fd_conflict\n     ++\t\tcd untracked &&\n     ++\t\t\"$@\" &&\n     ++\n     ++\t\t# Although we want pwd & git status to pass, test for existing\n     ++\t\t# rather than desired behavior.\n     ++\t\tif [[ $works == \"success\" ]]; then\n     ++\t\t\tpwd -P &&\n     ++\t\t\tgit status --porcelain\n     ++\t\telse\n     ++\t\t\t! pwd -P &&\n     ++\t\t\ttest_might_fail git status --porcelain\n     ++\t\tfi\n      +\t) &&\n     ++\ttest_path_is_missing empty &&\n     ++\ttest_path_is_missing untracked/random &&\n      +\n     -+\ttest_path_is_missing subdir/file.t &&\n     -+\ttest_path_is_dir subdir\n     ++\t# Although we want dir to be present, test for existing rather\n     ++\t# than desired behavior.\n     ++\tif [[ $works == \"success\" ]]; then\n     ++\t\ttest_path_is_dir untracked\n     ++\telse\n     ++\t\ttest_path_is_missing untracked\n     ++\tfi\n     ++}\n     ++\n     ++test_expect_success 'clean does not remove cwd incidentally' '\n     ++\ttest_incidental_untracked_dir_removal failure \\\n     ++\t\tgit -C .. clean -fd -e warnings . >warnings\n      +'\n      +\n     -+test_expect_failure 'rebase fails if cwd needs to be removed' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     -+\n     -+\tmkdir dirORfile &&\n     -+\t(\n     -+\t\tcd dirORfile &&\n     -+\t\ttest_must_fail git rebase foo/bar/baz fd_conflict 2>../error &&\n     -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n     -+\t) &&\n     -+\n     -+\ttest_path_is_dir dirORfile\n     ++test_expect_success 'stash does not remove cwd incidentally' '\n     ++\ttest_incidental_untracked_dir_removal failure \\\n     ++\t\tgit stash --include-untracked\n      +'\n      +\n     -+test_expect_failure 'revert does not remove cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success '`rm -rf dir` only removes a subset of dir' '\n     ++\ttest_when_finished \"rm -rf a/\" &&\n     ++\n     ++\tmkdir -p a/b/c &&\n     ++\t>a/b/c/untracked &&\n     ++\t>a/b/c/tracked &&\n     ++\tgit add a/b/c/tracked &&\n      +\n      +\t(\n     -+\t\tcd subdir &&\n     -+\t\tgit revert subdir/file\n     ++\t\tcd a/b &&\n     ++\t\tgit rm -rf ../b\n      +\t) &&\n      +\n     -+\ttest_path_is_missing subdir/file.t &&\n     -+\ttest_path_is_dir subdir\n     ++\ttest_path_is_dir a/b &&\n     ++\ttest_path_is_missing a/b/c/tracked &&\n     ++\ttest_path_is_file a/b/c/untracked\n      +'\n      +\n     -+test_expect_failure 'revert fails if cwd needs to be removed' '\n     -+\tgit checkout fd_conflict &&\n     -+\tgit revert HEAD &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n     ++\ttest_when_finished \"rm -rf a/\" &&\n     ++\n     ++\tmkdir -p a/b/c &&\n     ++\t>a/b/c/tracked &&\n     ++\tgit add a/b/c/tracked &&\n      +\n     -+\tmkdir dirORfile &&\n      +\t(\n     -+\t\tcd dirORfile &&\n     -+\t\ttest_must_fail git revert HEAD 2>../error &&\n     -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n     ++\t\tcd a/b &&\n     ++\t\tgit rm -rf ../b\n      +\t) &&\n      +\n     -+\ttest_path_is_dir dirORfile\n     ++\ttest_path_is_missing a/b/c/tracked &&\n     ++\t## We would prefer if a/b was still present, though empty, since it\n     ++\t## was the current working directory\n     ++\t#test_path_is_dir a/b\n     ++\t## But the current behavior is that it not only deletes the directory\n     ++\t## a/b as requested, but also goes and deletes a\n     ++\ttest_path_is_missing a\n      +'\n      +\n     -+test_expect_failure 'rm does not remove cwd incidentally' '\n     -+\ttest_when_finished \"git reset --hard\" &&\n     -+\tgit checkout foo/bar/baz &&\n     -+\n     ++test_expect_success 'git version continues working from a deleted dir' '\n     ++\tmkdir tmp &&\n      +\t(\n     -+\t\tcd foo &&\n     -+\t\tgit rm bar/baz.t\n     -+\t) &&\n     -+\n     -+\ttest_path_is_missing foo/bar/baz &&\n     -+\ttest_path_is_missing foo/bar &&\n     -+\ttest_path_is_dir foo\n     ++\t\tcd tmp &&\n     ++\t\trm -rf ../tmp &&\n     ++\t\tgit version\n     ++\t)\n      +'\n      +\n     -+test_expect_failure 'apply does not remove cwd incidentally' '\n     -+\ttest_when_finished \"git reset --hard\" &&\n     -+\tgit checkout foo/bar/baz &&\n     ++test_submodule_removal () {\n     ++\tpath_status=$1 &&\n     ++\tshift &&\n      +\n     -+\t(\n     -+\t\tcd subdir &&\n     -+\t\tgit diff subdir/file init | git apply\n     -+\t) &&\n     ++\ttest_status=\n     ++\ttest $path_status = dir && test_status=test_must_fail\n      +\n     -+\ttest_path_is_missing subdir/file.t &&\n     -+\ttest_path_is_dir subdir\n     -+'\n     ++\t# Actually, while path_status == dir && test_status=test_must_fail\n     ++\t# reflect our desired behavior, current behavior is:\n     ++\tpath_status=missing\n     ++\ttest_status=\n     ++\n     ++\ttest_when_finished \"git reset --hard HEAD~1\" &&\n     ++\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n      +\n     -+test_expect_failure 'clean does not remove cwd incidentally' '\n      +\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n      +\n     -+\tmkdir empty &&\n     -+\tmkdir untracked &&\n     -+\t>untracked/random &&\n     ++\tgit init my_submodule &&\n     ++\ttouch my_submodule/file &&\n     ++\tgit -C my_submodule add file &&\n     ++\tgit -C my_submodule commit -m \"initial commit\" &&\n     ++\tgit submodule add ./my_submodule &&\n     ++\tgit commit -m \"Add the submodule\" &&\n     ++\n      +\t(\n     -+\t\tcd untracked &&\n     -+\t\tgit clean -fd -e warnings :/ >../warnings &&\n     -+\t\tgrep \"Refusing to remove current working directory\" ../warnings\n     ++\t\tcd my_submodule &&\n     ++\t\t$test_status \"$@\"\n      +\t) &&\n      +\n     -+\ttest_path_is_missing empty &&\n     -+\ttest_path_is_missing untracked/random &&\n     -+\ttest_path_is_dir untracked\n     -+'\n     ++\ttest_path_is_${path_status} my_submodule\n     ++}\n      +\n     -+test_expect_failure 'stash does not remove cwd incidentally' '\n     -+\tgit checkout foo/bar/baz &&\n     -+\ttest_when_finished \"git clean -fdx\" &&\n     ++test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n     ++\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n     ++'\n      +\n     -+\tmkdir untracked &&\n     -+\t>untracked/random &&\n     -+\t(\n     -+\t\tcd untracked &&\n     -+\t\tgit stash --include-untracked &&\n     -+\t\tgit status\n     -+\t) &&\n     ++test_expect_success 'rm -r leaves submodule if cwd inside' '\n     ++\ttest_submodule_removal dir \\\n     ++\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n     ++'\n      +\n     -+\ttest_path_is_missing untracked/random &&\n     -+\ttest_path_is_dir untracked\n     ++test_expect_success 'rm -rf removes submodule even if cwd inside' '\n     ++\ttest_submodule_removal missing \\\n     ++\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n      +'\n      +\n      +test_done\n  2:  f6129a8ac9c !  2:  200ddece05d setup: introduce startup_info->original_cwd\n     @@ cache.h: void overlay_tree_on_index(struct index_state *istate,\n      +\tconst char *original_cwd;\n       };\n       extern struct startup_info *startup_info;\n     ++extern const char *tmp_original_cwd;\n       \n     + /* merge.c */\n     + struct commit_list;\n      \n     - ## git.c ##\n     -@@ git.c: int cmd_main(int argc, const char **argv)\n     + ## common-main.c ##\n     +@@ common-main.c: static void restore_sigpipe_to_default(void)\n     + int main(int argc, const char **argv)\n     + {\n     + \tint result;\n     ++\tstruct strbuf tmp = STRBUF_INIT;\n     + \n     + \ttrace2_initialize_clock();\n       \n     - \ttrace_command_performance(argv);\n     +@@ common-main.c: int main(int argc, const char **argv)\n     + \ttrace2_cmd_start(argv);\n     + \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n       \n     -+\tstartup_info->original_cwd = xgetcwd();\n     ++\tif (!strbuf_getcwd(&tmp))\n     ++\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n      +\n     - \t/*\n     - \t * \"git-xxxx\" is the same as \"git xxxx\", but we obviously:\n     - \t *\n     + \tresult = cmd_main(argc, argv);\n     + \n     + \ttrace2_cmd_exit(result);\n      \n       ## setup.c ##\n     +@@ setup.c: static int work_tree_config_is_bogus;\n     + \n     + static struct startup_info the_startup_info;\n     + struct startup_info *startup_info = &the_startup_info;\n     ++const char *tmp_original_cwd;\n     + \n     + /*\n     +  * The input parameter must contain an absolute path, and it must already be\n      @@ setup.c: void setup_work_tree(void)\n       \tinitialized = 1;\n       }\n     @@ setup.c: void setup_work_tree(void)\n      +\tconst char *worktree = NULL;\n      +\tint offset = -1;\n      +\n     -+\t/*\n     -+\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n     -+\t * we're an auxiliary tool like git-remote-http or test-tool.\n     -+\t */\n     -+\tif (!startup_info->original_cwd)\n     ++\tif (!tmp_original_cwd)\n      +\t\treturn;\n      +\n      +\t/*\n      +\t * startup_info->original_cwd points to the current working\n      +\t * directory we inherited from our parent process, which is a\n     -+\t * directory we want to avoid incidentally removing.\n     ++\t * directory we want to avoid removing.\n      +\t *\n      +\t * For convience, we would like to have the path relative to the\n      +\t * worktree instead of an absolute path.\n     @@ setup.c: void setup_work_tree(void)\n      +\t */\n      +\n      +\t/* Normalize the directory */\n     -+\tstrbuf_realpath(&tmp, startup_info->original_cwd, 1);\n     -+\tfree((char*)startup_info->original_cwd);\n     ++\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n     ++\tfree((char*)tmp_original_cwd);\n     ++\ttmp_original_cwd = NULL;\n      +\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n      +\n     -+\t/* Find out if this is in the worktree */\n     ++\t/*\n     ++\t * Get our worktree; we only protect the current working directory\n     ++\t * if it's in the worktree.\n     ++\t */\n      +\tworktree = get_git_work_tree();\n     -+\tif (worktree)\n     -+\t\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n     ++\tif (!worktree)\n     ++\t\tgoto no_prevention_needed;\n     ++\n     ++\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n      +\tif (offset >= 0) {\n      +\t\t/*\n     ++\t\t * If startup_info->original_cwd == worktree, that is already\n     ++\t\t * protected and we don't need original_cwd as a secondary\n     ++\t\t * protection measure.\n     ++\t\t */\n     ++\t\tif (!*(startup_info->original_cwd + offset))\n     ++\t\t\tgoto no_prevention_needed;\n     ++\n     ++\t\t/*\n      +\t\t * original_cwd was inside worktree; precompose it just as\n      +\t\t * we do prefix so that built up paths will match\n      +\t\t */\n     @@ setup.c: void setup_work_tree(void)\n      +\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n      +\t\t\t\t\t\t    + offset);\n      +\t}\n     ++\treturn;\n     ++\n     ++no_prevention_needed:\n     ++\tfree((char*)startup_info->original_cwd);\n     ++\tstartup_info->original_cwd = NULL;\n      +}\n      +\n       static int read_worktree_config(const char *var, const char *value, void *vdata)\n  3:  e74975e83cc !  3:  68ae90546fe unpack-trees: refuse to remove startup_info->original_cwd\n     @@ Commit message\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh ##\n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'checkout does not clean cwd incidentally' '\n     - \ttest_path_is_dir foo\n     +@@ t/t2501-cwd-empty.sh: test_expect_success 'checkout does not clean cwd incidentally' '\n       '\n       \n     --test_expect_failure 'checkout fails if cwd needs to be removed' '\n     -+test_expect_success 'checkout fails if cwd needs to be removed' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'checkout fails if cwd needs to be removed' '\n     +-\ttest_required_dir_removal failure git checkout fd_conflict\n     ++\ttest_required_dir_removal success git checkout fd_conflict\n     + '\n     + \n     + test_expect_success 'reset --hard does not clean cwd incidentally' '\n     +@@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n     + \t(\n     + \t\tcd dirORfile &&\n     + \n     +-\t\t# We would rather this failed, but we test for existing\n     +-\t\t# rather than desired behavior\n     +-\t\tgit merge fd_conflict 2>../error\n     ++\t\ttest_must_fail git merge fd_conflict 2>../error\n     + \t) &&\n       \n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'merge does not remove cwd incidentally' '\n     - \ttest_path_is_dir subdir\n     +-\t## Here is the behavior we would rather have:\n     +-\t#test_path_is_dir dirORfile &&\n     +-\t#grep \"Refusing to remove the current working directory\" error\n     +-\t## But instead we test for existing behavior\n     +-\ttest_path_is_file dirORfile &&\n     +-\ttest_must_be_empty error\n     ++\ttest_path_is_dir dirORfile &&\n     ++\tgrep \"Refusing to remove the current working directory\" error\n       '\n       \n     --test_expect_failure 'merge fails if cwd needs to be removed' '\n     -+test_expect_success 'merge fails if cwd needs to be removed' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + GIT_TEST_MERGE_ALGORITHM=ort\n       \n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n     - \ttest_path_is_dir subdir\n     + test_expect_success 'merge fails if cwd needs to be removed' '\n     +-\ttest_required_dir_removal failure git merge fd_conflict\n     ++\ttest_required_dir_removal success git merge fd_conflict\n       '\n       \n     --test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n     -+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'cherry-pick does not clean cwd incidentally' '\n     +@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick does not clean cwd incidentally' '\n     + '\n       \n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rebase does not remove cwd incidentally' '\n     - \ttest_path_is_dir subdir\n     + test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n     +-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n     ++\ttest_required_dir_removal success git cherry-pick fd_conflict\n       '\n       \n     --test_expect_failure 'rebase fails if cwd needs to be removed' '\n     -+test_expect_success 'rebase fails if cwd needs to be removed' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'rebase does not clean cwd incidentally' '\n     +@@ t/t2501-cwd-empty.sh: test_expect_success 'revert does not clean cwd incidentally' '\n     + '\n       \n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'revert does not remove cwd incidentally' '\n     - \ttest_path_is_dir subdir\n     + test_expect_success 'revert fails if cwd needs to be removed' '\n     +-\ttest_required_dir_removal failure git revert undo_fd_conflict\n     ++\ttest_required_dir_removal success git revert undo_fd_conflict\n       '\n       \n     --test_expect_failure 'revert fails if cwd needs to be removed' '\n     -+test_expect_success 'revert fails if cwd needs to be removed' '\n     - \tgit checkout fd_conflict &&\n     - \tgit revert HEAD &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'rm does not clean cwd incidentally' '\n      \n       ## unpack-trees.c ##\n      @@ unpack-trees.c: static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n  4:  e06806e3a32 !  4:  1bb8905900c unpack-trees: add special cwd handling\n     @@ Commit message\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh ##\n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'reset --hard does not clean cwd incidentally' '\n     - \ttest_path_is_dir foo\n     +@@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard does not clean cwd incidentally' '\n       '\n       \n     --test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n     -+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'reset --hard fails if cwd needs to be removed' '\n     +-\ttest_required_dir_removal failure git reset --hard fd_conflict\n     ++\ttest_required_dir_removal success git reset --hard fd_conflict\n     + '\n       \n     + test_expect_success 'merge does not clean cwd incidentally' '\n      \n       ## unpack-trees.c ##\n      @@ unpack-trees.c: static int verify_absent_1(const struct cache_entry *ce,\n  5:  46728f74ea1 !  5:  8a33d74e7cf symlinks: do not include startup_info->original_cwd in dir removal\n     @@ symlinks.c: void schedule_dir_for_removal(const char *name, int len)\n       \t\t\t\t   &previous_slash);\n      \n       ## t/t2501-cwd-empty.sh ##\n     -@@ t/t2501-cwd-empty.sh: test_expect_success setup '\n     - \tgit commit -m dirORfile\n     - '\n     +@@ t/t2501-cwd-empty.sh: test_required_dir_removal () {\n     + }\n       \n     --test_expect_failure 'checkout does not clean cwd incidentally' '\n     -+test_expect_success 'checkout does not clean cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_path_is_dir foo/bar &&\n     + test_expect_success 'checkout does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git checkout init\n     ++\ttest_incidental_dir_removal success git checkout init\n     + '\n       \n     + test_expect_success 'checkout fails if cwd needs to be removed' '\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'checkout fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n       '\n       \n     --test_expect_failure 'reset --hard does not clean cwd incidentally' '\n     -+test_expect_success 'reset --hard does not clean cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_path_is_dir foo/bar &&\n     + test_expect_success 'reset --hard does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git reset --hard init\n     ++\ttest_incidental_dir_removal success git reset --hard init\n     + '\n       \n     + test_expect_success 'reset --hard fails if cwd needs to be removed' '\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n       '\n       \n     --test_expect_failure 'merge does not remove cwd incidentally' '\n     -+test_expect_success 'merge does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'merge does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git merge reverted\n     ++\ttest_incidental_dir_removal success git merge reverted\n     + '\n       \n     + # This file uses some simple merges where\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n       '\n       \n     --test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n     -+test_expect_success 'cherry-pick does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     - \n     -@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n     + test_expect_success 'cherry-pick does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git cherry-pick reverted\n     ++\ttest_incidental_dir_removal success git cherry-pick reverted\n       '\n       \n     --test_expect_failure 'rebase does not remove cwd incidentally' '\n     -+test_expect_success 'rebase does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     - \n     + test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'rebase fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n       '\n       \n     --test_expect_failure 'revert does not remove cwd incidentally' '\n     -+test_expect_success 'revert does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'revert does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git revert HEAD\n     ++\ttest_incidental_dir_removal success git revert HEAD\n     + '\n       \n     + test_expect_success 'revert fails if cwd needs to be removed' '\n  6:  01ce9444dae !  6:  11e4ec881bb clean: do not attempt to remove startup_info->original_cwd\n     @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n      +\t\tstrbuf_realpath(&realpath, path->buf, 1);\n      +\n      +\t\t/*\n     -+\t\t * path and realpath are absolute; for comparison, we want\n     -+\t\t * startup_info->original_cwd to be an absolute path too.  We\n     -+\t\t * can use strbuf_realpath for this.  Also, if original_cwd\n     -+\t\t * started out as the empty string, then it corresponded to\n     -+\t\t * the top of the worktree, which is protected by other means\n     -+\t\t * so we just leave it blank.\n     ++\t\t * path and realpath are absolute; for comparison, we would\n     ++\t\t * like to transform startup_info->original_cwd to an absolute\n     ++\t\t * path too.\n      +\t\t */\n     -+\t\t if (*startup_info->original_cwd)\n     ++\t\t if (startup_info->original_cwd)\n      +\t\t\t strbuf_realpath(&real_ocwd,\n      +\t\t\t\t\t startup_info->original_cwd, 1);\n      +\n     @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n       \treturn ret;\n      \n       ## t/t2501-cwd-empty.sh ##\n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'apply does not remove cwd incidentally' '\n     - \ttest_path_is_dir subdir\n     - '\n     +@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n     + }\n       \n     --test_expect_failure 'clean does not remove cwd incidentally' '\n     -+test_expect_success 'clean does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'clean does not remove cwd incidentally' '\n     +-\ttest_incidental_untracked_dir_removal failure \\\n     +-\t\tgit -C .. clean -fd -e warnings . >warnings\n     ++\ttest_incidental_untracked_dir_removal success \\\n     ++\t\tgit -C .. clean -fd -e warnings . >warnings &&\n     ++\tgrep \"Refusing to remove current working directory\" warnings\n     + '\n       \n     + test_expect_success 'stash does not remove cwd incidentally' '\n  -:  ----------- >  7:  39b1f3a225e rebase: do not attempt to remove startup_info->original_cwd\n  7:  edec0894ca2 !  8:  0110462a19c stash: do not attempt to remove startup_info->original_cwd\n     @@ Metadata\n       ## Commit message ##\n          stash: do not attempt to remove startup_info->original_cwd\n      \n     +    Since stash spawns a `clean` subprocess, make sure we run that from the\n     +    startup_info->original_cwd directory, so that the `clean` processs knows\n     +    to protect that directory.  Also, since the `clean` command might no\n     +    longer run from the toplevel, pass the ':/' magic pathspec to ensure we\n     +    still clean from the toplevel.\n     +\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/stash.c ##\n     @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char\n       \n       \t\t\tcp.git_cmd = 1;\n      +\t\t\tif (startup_info->original_cwd &&\n     -+\t\t\t    *startup_info->original_cwd &&\n      +\t\t\t    !is_absolute_path(startup_info->original_cwd))\n      +\t\t\t\tcp.dir = startup_info->original_cwd;\n       \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n     @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char\n      \n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'clean does not remove cwd incidentally' '\n     - \ttest_path_is_dir untracked\n       '\n       \n     --test_expect_failure 'stash does not remove cwd incidentally' '\n     -+test_expect_success 'stash does not remove cwd incidentally' '\n     - \tgit checkout foo/bar/baz &&\n     - \ttest_when_finished \"git clean -fdx\" &&\n     + test_expect_success 'stash does not remove cwd incidentally' '\n     +-\ttest_incidental_untracked_dir_removal failure \\\n     ++\ttest_incidental_untracked_dir_removal success \\\n     + \t\tgit stash --include-untracked\n     + '\n       \n  8:  1815f18592b !  9:  2c73a09a2e8 dir: avoid incidentally removing the original_cwd in remove_path()\n     @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n      \n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_expect_success 'revert fails if cwd needs to be removed' '\n     - \ttest_path_is_dir dirORfile\n       '\n       \n     --test_expect_failure 'rm does not remove cwd incidentally' '\n     -+test_expect_success 'rm does not remove cwd incidentally' '\n     - \ttest_when_finished \"git reset --hard\" &&\n     - \tgit checkout foo/bar/baz &&\n     + test_expect_success 'rm does not clean cwd incidentally' '\n     +-\ttest_incidental_dir_removal failure git rm bar/baz.t\n     ++\ttest_incidental_dir_removal success git rm bar/baz.t\n     + '\n       \n     -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rm does not remove cwd incidentally' '\n     - \ttest_path_is_dir foo\n     + test_expect_success 'apply does not remove cwd incidentally' '\n     + \tgit diff HEAD HEAD~1 >patch &&\n     +-\ttest_incidental_dir_removal failure git apply ../patch\n     ++\ttest_incidental_dir_removal success git apply ../patch\n       '\n       \n     --test_expect_failure 'apply does not remove cwd incidentally' '\n     -+test_expect_success 'apply does not remove cwd incidentally' '\n     - \ttest_when_finished \"git reset --hard\" &&\n     - \tgit checkout foo/bar/baz &&\n     + test_incidental_untracked_dir_removal () {\n     +@@ t/t2501-cwd-empty.sh: test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n     + \t) &&\n     + \n     + \ttest_path_is_missing a/b/c/tracked &&\n     +-\t## We would prefer if a/b was still present, though empty, since it\n     +-\t## was the current working directory\n     +-\t#test_path_is_dir a/b\n     +-\t## But the current behavior is that it not only deletes the directory\n     +-\t## a/b as requested, but also goes and deletes a\n     +-\ttest_path_is_missing a\n     ++\ttest_path_is_missing a/b/c &&\n     ++\ttest_path_is_dir a/b\n     + '\n       \n     + test_expect_success 'git version continues working from a deleted dir' '\n  9:  adaad7aeaac ! 10:  d4e50b4053d dir: new flag to remove_dir_recurse() to spare the original_cwd\n     @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n        * of the above REMOVE_DIR_* constants. Return 0 on success.\n      \n       ## t/t2501-cwd-empty.sh ##\n     -@@ t/t2501-cwd-empty.sh: test_expect_success 'stash does not remove cwd incidentally' '\n     - \ttest_path_is_dir untracked\n     - '\n     +@@ t/t2501-cwd-empty.sh: test_submodule_removal () {\n     + \ttest_status=\n     + \ttest $path_status = dir && test_status=test_must_fail\n     + \n     +-\t# Actually, while path_status == dir && test_status=test_must_fail\n     +-\t# reflect our desired behavior, current behavior is:\n     +-\tpath_status=missing\n     +-\ttest_status=\n     +-\n     + \ttest_when_finished \"git reset --hard HEAD~1\" &&\n     + \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n       \n     -+test_expect_success 'rm -r leaves submodule if cwd inside' '\n     -+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n     -+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n     -+\n     -+\tgit checkout foo/bar/baz &&\n     -+\n     -+\tgit init my_submodule &&\n     -+\ttouch my_submodule/file &&\n     -+\tgit -C my_submodule add file &&\n     -+\tgit -C my_submodule commit -m \"initial commit\" &&\n     -+\tgit submodule add ./my_submodule &&\n     -+\tgit commit -m \"Add the submodule\" &&\n     -+\n     -+\t(\n     -+\t\tcd my_submodule &&\n     -+\t\ttest_must_fail git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n     -+\t) &&\n     -+\n     -+\ttest_path_is_dir my_submodule\n     -+'\n     -+\n     -+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n     -+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n     -+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n     -+\n     -+\tgit checkout foo/bar/baz &&\n     -+\n     -+\tgit init my_submodule &&\n     -+\ttouch my_submodule/file &&\n     -+\tgit -C my_submodule add file &&\n     -+\tgit -C my_submodule commit -m \"initial commit\" &&\n     -+\tgit submodule add ./my_submodule &&\n     -+\tgit commit -m \"Add the submodule\" &&\n     -+\n     -+\t(\n     -+\t\tcd my_submodule &&\n     -+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n     -+\t) &&\n     -+\n     -+\ttest_path_is_missing my_submodule\n     -+'\n     -+\n     - test_done\n  -:  ----------- > 11:  7eb6281be4b t2501: simplify the tests since we can now assume desired behavior\n\n-- \ngitgitgadget\n"},{"id":"442380","messageId":"4b0044656b0ebe81db60f605a184948f728c4b9d.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:52Z","receivedAt":"2021-11-26T22:45:08Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove directories left empty as a \"convenience\"\nafter removing files within them.  That is normally fine, but removing\nthe current working directory can be rather inconvenient since it can\ncause confusion for the user when they run subsequent commands.  For\nexample, after one git process has removed the current working\ndirectory, git status/log/diff will all abort with the message:\n\n    fatal: Unable to read current working directory: No such file or directory\n\nWe also have code paths that, when a file needs to be placed where a\ndirectory is (due to e.g. checkout, merge, reset, whatever), will check\nif this is okay and error out if not.  These rules include:\n  * all tracked files under that directory are intended to be removed by\n    the operation\n  * none of the tracked files under that directory have uncommitted\n    modification\n  * there are no untracked files under that directory\nHowever, if we end up remove the current working directory, we can cause\nuser confusion when they run subsequent commands, so we would prefer if\nthere was a fourth rule added to this list: avoid removing the current\nworking directory.\n\nSince there are several code paths that can result in the current\nworking directory being removed, add several tests of various different\ncodepaths.  To make it clearer what the difference between the current\nbehavior and the behavior at the end of the series, code both of them\ninto the tests and have the appropriate behavior be selected by a flag.\nSubsequent commits will toggle the flag from current to desired\nbehavior.\n\nAlso add a few tests suggested during the review of earlier rounds of\nthis patch series.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 333 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 333 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..67feb6fd200\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,333 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tmkdir dirORfile &&\n+\ttest_commit dirORfile/foo &&\n+\n+\tgit rm -r dirORfile &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile &&\n+\n+\tgit switch -c df_conflict HEAD~1 &&\n+\ttest_commit random_file &&\n+\n+\tgit switch -c undo_fd_conflict fd_conflict &&\n+\tgit revert HEAD\n+'\n+\n+test_incidental_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif [[ $works == \"success\" ]]; then\n+\t\t\tpwd -P &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! pwd -P &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif [[ $works == \"success\" ]]; then\n+\t\ttest_path_is_dir foo\n+\telse\n+\t\ttest_path_is_missing foo\n+\tfi\n+}\n+\n+test_required_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\tgit checkout df_conflict^{commit} &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We'd like for the command to fail (much as it would if there\n+\t\t# was an untracked file there), and for pwd & git status to\n+\t\t# succeed afterwards.  But test for existing rather than\n+\t\t# desired behavior.\n+\t\tif [[ $works == \"success\" ]]; then\n+\t\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\t\t\tpwd -P &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t\"$@\" &&\n+\t\t\t! pwd -P &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\n+\t# Although we want dirORfile to be present, test for existing rather\n+\t# than desired behavior.\n+\tif [[ $works == \"success\" ]]; then\n+\t\ttest_path_is_dir dirORfile\n+\telse\n+\t\ttest_path_is_file dirORfile\n+\tfi\n+}\n+\n+test_expect_success 'checkout does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git checkout init\n+'\n+\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git checkout fd_conflict\n+'\n+\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git reset --hard init\n+'\n+\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git reset --hard fd_conflict\n+'\n+\n+test_expect_success 'merge does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git merge reverted\n+'\n+\n+# This file uses some simple merges where\n+#   Base: 'dirORfile/' exists\n+#   Side1: random other file changed\n+#   Side2: 'dirORfile/' removed, 'dirORfile' added\n+# this should resolve cleanly, but merge-recursive throws merge conflicts\n+# because it's dumb.  Add a special test for checking merge-recursive (and\n+# merge-ort), then after this just hard require ort for all remaining tests.\n+#\n+test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We would rather this failed, but we test for existing\n+\t\t# rather than desired behavior\n+\t\tgit merge fd_conflict 2>../error\n+\t) &&\n+\n+\t## Here is the behavior we would rather have:\n+\t#test_path_is_dir dirORfile &&\n+\t#grep \"Refusing to remove the current working directory\" error\n+\t## But instead we test for existing behavior\n+\ttest_path_is_file dirORfile &&\n+\ttest_must_be_empty error\n+'\n+\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n+test_expect_success 'merge fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git merge fd_conflict\n+'\n+\n+test_expect_success 'cherry-pick does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git cherry-pick reverted\n+'\n+\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+'\n+\n+test_expect_success 'rebase does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rebase reverted\n+'\n+\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git rebase fd_conflict\n+'\n+\n+test_expect_success 'revert does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git revert HEAD\n+'\n+\n+test_expect_success 'revert fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git revert undo_fd_conflict\n+'\n+\n+test_expect_success 'rm does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rm bar/baz.t\n+'\n+\n+test_expect_success 'apply does not remove cwd incidentally' '\n+\tgit diff HEAD HEAD~1 >patch &&\n+\ttest_incidental_dir_removal failure git apply ../patch\n+'\n+\n+test_incidental_untracked_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\tmkdir -p untracked &&\n+\tmkdir empty\n+\t>untracked/random &&\n+\n+\t(\n+\t\tcd untracked &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif [[ $works == \"success\" ]]; then\n+\t\t\tpwd -P &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! pwd -P &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif [[ $works == \"success\" ]]; then\n+\t\ttest_path_is_dir untracked\n+\telse\n+\t\ttest_path_is_missing untracked\n+\tfi\n+}\n+\n+test_expect_success 'clean does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings\n+'\n+\n+test_expect_success 'stash does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit stash --include-untracked\n+'\n+\n+test_expect_success '`rm -rf dir` only removes a subset of dir' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/untracked &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_dir a/b &&\n+\ttest_path_is_missing a/b/c/tracked &&\n+\ttest_path_is_file a/b/c/untracked\n+'\n+\n+test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_missing a/b/c/tracked &&\n+\t## We would prefer if a/b was still present, though empty, since it\n+\t## was the current working directory\n+\t#test_path_is_dir a/b\n+\t## But the current behavior is that it not only deletes the directory\n+\t## a/b as requested, but also goes and deletes a\n+\ttest_path_is_missing a\n+'\n+\n+test_expect_success 'git version continues working from a deleted dir' '\n+\tmkdir tmp &&\n+\t(\n+\t\tcd tmp &&\n+\t\trm -rf ../tmp &&\n+\t\tgit version\n+\t)\n+'\n+\n+test_submodule_removal () {\n+\tpath_status=$1 &&\n+\tshift &&\n+\n+\ttest_status=\n+\ttest $path_status = dir && test_status=test_must_fail\n+\n+\t# Actually, while path_status == dir && test_status=test_must_fail\n+\t# reflect our desired behavior, current behavior is:\n+\tpath_status=missing\n+\ttest_status=\n+\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\t$test_status \"$@\"\n+\t) &&\n+\n+\ttest_path_is_${path_status} my_submodule\n+}\n+\n+test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n+'\n+\n+test_expect_success 'rm -r leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n+'\n+\n+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n+\ttest_submodule_removal missing \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"442381","messageId":"200ddece05d3d0599b16897ff9533cdfb3324b0c.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:53Z","receivedAt":"2021-11-26T22:45:10Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands run from that directory to get confused and fail with a message\nabout being unable to read the current working directory:\n\n    $ git status\n    fatal: Unable to read current working directory: No such file or directory\n\nNon-git commands likely have similar warnings or even errors, e.g.\n\n    $ bash -c 'echo hello'\n    shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n    hello\n\nThis confuses end users, particularly since the command they get the\nerror from is not the one that caused the problem; the problem came from\nthe side-effect of some previous command.\n\nWe would like to avoid removing the current working directory of our\nparent process; towards this end, introduce a new variable,\nstartup_info->original_cwd, that tracks the current working directory\nthat we inherited from our parent process.  For convenience of later\ncomparisons, we prefer that this new variable store a path relative to\nthe toplevel working directory (thus much like 'prefix'), except without\nthe trailing slash.\n\nSubsequent commits will make use of this new variable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n cache.h       |  2 ++\n common-main.c |  4 ++++\n setup.c       | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex eba12487b99..92e181ea759 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1834,8 +1834,10 @@ void overlay_tree_on_index(struct index_state *istate,\n struct startup_info {\n \tint have_repository;\n \tconst char *prefix;\n+\tconst char *original_cwd;\n };\n extern struct startup_info *startup_info;\n+extern const char *tmp_original_cwd;\n \n /* merge.c */\n struct commit_list;\ndiff --git a/common-main.c b/common-main.c\nindex 71e21dd20a3..aa8d5aba5bb 100644\n--- a/common-main.c\n+++ b/common-main.c\n@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)\n int main(int argc, const char **argv)\n {\n \tint result;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \n \ttrace2_initialize_clock();\n \n@@ -49,6 +50,9 @@ int main(int argc, const char **argv)\n \ttrace2_cmd_start(argv);\n \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n \n+\tif (!strbuf_getcwd(&tmp))\n+\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n+\n \tresult = cmd_main(argc, argv);\n \n \ttrace2_cmd_exit(result);\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..44f5bd38f7b 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -12,6 +12,7 @@ static int work_tree_config_is_bogus;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n+const char *tmp_original_cwd;\n \n /*\n  * The input parameter must contain an absolute path, and it must already be\n@@ -432,6 +433,69 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static void setup_original_cwd(void)\n+{\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tconst char *worktree = NULL;\n+\tint offset = -1;\n+\n+\tif (!tmp_original_cwd)\n+\t\treturn;\n+\n+\t/*\n+\t * startup_info->original_cwd points to the current working\n+\t * directory we inherited from our parent process, which is a\n+\t * directory we want to avoid removing.\n+\t *\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n+\t *\n+\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+\t * but differs in two ways:\n+\t *   - prefix has a trailing '/'\n+\t *   - if the user passes '-C' to git, that modifies the prefix but\n+\t *     not startup_info->original_cwd.\n+\t */\n+\n+\t/* Normalize the directory */\n+\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n+\tfree((char*)tmp_original_cwd);\n+\ttmp_original_cwd = NULL;\n+\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n+\n+\t/*\n+\t * Get our worktree; we only protect the current working directory\n+\t * if it's in the worktree.\n+\t */\n+\tworktree = get_git_work_tree();\n+\tif (!worktree)\n+\t\tgoto no_prevention_needed;\n+\n+\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n+\tif (offset >= 0) {\n+\t\t/*\n+\t\t * If startup_info->original_cwd == worktree, that is already\n+\t\t * protected and we don't need original_cwd as a secondary\n+\t\t * protection measure.\n+\t\t */\n+\t\tif (!*(startup_info->original_cwd + offset))\n+\t\t\tgoto no_prevention_needed;\n+\n+\t\t/*\n+\t\t * original_cwd was inside worktree; precompose it just as\n+\t\t * we do prefix so that built up paths will match\n+\t\t */\n+\t\tstartup_info->original_cwd = \\\n+\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n+\t\t\t\t\t\t    + offset);\n+\t}\n+\treturn;\n+\n+no_prevention_needed:\n+\tfree((char*)startup_info->original_cwd);\n+\tstartup_info->original_cwd = NULL;\n+}\n+\n static int read_worktree_config(const char *var, const char *value, void *vdata)\n {\n \tstruct repository_format *data = vdata;\n@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n+\tsetup_original_cwd();\n \n \tstrbuf_release(&dir);\n \tstrbuf_release(&gitdir);\n-- \ngitgitgadget\n\n"},{"id":"442382","messageId":"68ae90546fed39fbe58edec8a1b81e3054b0c224.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 03/11] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:54Z","receivedAt":"2021-11-26T22:45:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory we inherited\nfrom our parent process.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 20 +++++++-------------\n unpack-trees.c       | 17 +++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 21 insertions(+), 17 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 67feb6fd200..52399d1906f 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -106,7 +106,7 @@ test_expect_success 'checkout does not clean cwd incidentally' '\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git checkout fd_conflict\n+\ttest_required_dir_removal success git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n@@ -137,23 +137,17 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We would rather this failed, but we test for existing\n-\t\t# rather than desired behavior\n-\t\tgit merge fd_conflict 2>../error\n+\t\ttest_must_fail git merge fd_conflict 2>../error\n \t) &&\n \n-\t## Here is the behavior we would rather have:\n-\t#test_path_is_dir dirORfile &&\n-\t#grep \"Refusing to remove the current working directory\" error\n-\t## But instead we test for existing behavior\n-\ttest_path_is_file dirORfile &&\n-\ttest_must_be_empty error\n+\ttest_path_is_dir dirORfile &&\n+\tgrep \"Refusing to remove the current working directory\" error\n '\n \n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git merge fd_conflict\n+\ttest_required_dir_removal success git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n@@ -161,7 +155,7 @@ test_expect_success 'cherry-pick does not clean cwd incidentally' '\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+\ttest_required_dir_removal success git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n@@ -177,7 +171,7 @@ test_expect_success 'revert does not clean cwd incidentally' '\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git revert undo_fd_conflict\n+\ttest_required_dir_removal success git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..6bc16f3a714 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose startup_info->original_cwd */\n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"442383","messageId":"1bb8905900ca4170db4d9a9211d56d01b5013eda.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 04/11] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:55Z","receivedAt":"2021-11-26T22:45:13Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the_original_cwd\nbeing in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\nthere is no untracked checking to be done, so we simply add a special\ncase near the top of verify_absent_1.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 13 +++++++++++--\n 2 files changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 52399d1906f..886a391a63d 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -114,7 +114,7 @@ test_expect_success 'reset --hard does not clean cwd incidentally' '\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git reset --hard fd_conflict\n+\ttest_required_dir_removal success git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 6bc16f3a714..5852807d2fb 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking startup_info->original_cwd... */\n+\t\tif (startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"442384","messageId":"8a33d74e7cff636c04abf0198bb7d5c72d118d35.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 05/11] symlinks: do not include startup_info->original_cwd in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:56Z","receivedAt":"2021-11-26T22:45:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude startup_info->original_cwd and leave it around.  This\navoids the user getting confused by subsequent git commands (and non-git\ncommands) that would otherwise report confusing messages about being\nunable to read the current working directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           |  8 +++++++-\n t/t2501-cwd-empty.sh | 10 +++++-----\n 2 files changed, 12 insertions(+), 6 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..c667baa949b 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif ((startup_info->original_cwd &&\n+\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n+\t\t    rmdir(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(name, startup_info->original_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 886a391a63d..496f4c6a6e9 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -102,7 +102,7 @@ test_required_dir_removal () {\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git checkout init\n+\ttest_incidental_dir_removal success git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n@@ -110,7 +110,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git reset --hard init\n+\ttest_incidental_dir_removal success git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n@@ -118,7 +118,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git merge reverted\n+\ttest_incidental_dir_removal success git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -151,7 +151,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git cherry-pick reverted\n+\ttest_incidental_dir_removal success git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n@@ -167,7 +167,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git revert HEAD\n+\ttest_incidental_dir_removal success git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-- \ngitgitgadget\n\n"},{"id":"442385","messageId":"11e4ec881bb8c23f981054902d5bac09cadaad4d.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 06/11] clean: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:57Z","receivedAt":"2021-11-26T22:45:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 44 +++++++++++++++++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  5 +++--\n 2 files changed, 38 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..3ff02bbbffe 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n {\n \tDIR *dir;\n \tstruct strbuf quoted = STRBUF_INIT;\n+\tstruct strbuf realpath = STRBUF_INIT;\n+\tstruct strbuf real_ocwd = STRBUF_INIT;\n \tstruct dirent *e;\n \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n \tstruct string_list dels = STRING_LIST_INIT_DUP;\n@@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t/*\n+\t\t * Normalize path components in path->buf, e.g. change '\\' to\n+\t\t * '/' on Windows.\n+\t\t */\n+\t\tstrbuf_realpath(&realpath, path->buf, 1);\n+\n+\t\t/*\n+\t\t * path and realpath are absolute; for comparison, we would\n+\t\t * like to transform startup_info->original_cwd to an absolute\n+\t\t * path too.\n+\t\t */\n+\t\t if (startup_info->original_cwd)\n+\t\t\t strbuf_realpath(&real_ocwd,\n+\t\t\t\t\t startup_info->original_cwd, 1);\n+\n+\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n \t}\n out:\n+\tstrbuf_release(&realpath);\n+\tstrbuf_release(&real_ocwd);\n \tstrbuf_release(&quoted);\n \tstring_list_clear(&dels, 0);\n \treturn ret;\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 496f4c6a6e9..be04b673f6a 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -221,8 +221,9 @@ test_incidental_untracked_dir_removal () {\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n-\t\tgit -C .. clean -fd -e warnings . >warnings\n+\ttest_incidental_untracked_dir_removal success \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings &&\n+\tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442386","messageId":"d4e50b4053d37a1a57ba552ae42bbd6921a719f9.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:41:01Z","receivedAt":"2021-11-26T22:45:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nremove_dir_recurse(), and its non-static wrapper called\nremove_dir_recursively(), both take flags for modifying its behavior.\nAs with the previous commits, we would generally like to protect\nthe original_cwd, but we want to forced user commands (e.g. 'git rm -rf\n...') or other special cases to remove it.  Add a flag for this purpose.\nAfter reading through every caller of remove_dir_recursively() in the\ncurrent codebase, there was only one that should be adjusted and that\none only in a very unusual circumstance.  Add a pair of new testcases to\nhighlight that very specific case involving submodules && --git-dir &&\n--work-tree.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/rm.c         |  3 ++-\n dir.c                | 12 +++++++++---\n dir.h                |  3 +++\n t/t2501-cwd-empty.sh |  5 -----\n 4 files changed, 14 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/rm.c b/builtin/rm.c\nindex 3d0967cdc11..b4132e5d8ee 100644\n--- a/builtin/rm.c\n+++ b/builtin/rm.c\n@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)\n \tif (!index_only) {\n \t\tint removed = 0, gitmodules_modified = 0;\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tint flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;\n \t\tfor (i = 0; i < list.nr; i++) {\n \t\t\tconst char *path = list.entry[i].name;\n \t\t\tif (list.entry[i].is_submodule) {\n \t\t\t\tstrbuf_reset(&buf);\n \t\t\t\tstrbuf_addstr(&buf, path);\n-\t\t\t\tif (remove_dir_recursively(&buf, 0))\n+\t\t\t\tif (remove_dir_recursively(&buf, flag))\n \t\t\t\t\tdie(_(\"could not remove '%s'\"), path);\n \n \t\t\t\tremoved = 1;\ndiff --git a/dir.c b/dir.c\nindex 97d6b71c872..52064345a6b 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tint ret = 0, original_len = path->len, len, kept_down = 0;\n \tint only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);\n \tint keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);\n+\tint purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);\n \tstruct object_id submodule_head;\n \n \tif ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&\n@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (!purge_original_cwd &&\n+\t\t    startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\ndiff --git a/dir.h b/dir.h\nindex d6a5d03bec2..8e02dfb505d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n /* Remove the contents of path, but leave path itself. */\n #define REMOVE_DIR_KEEP_TOPLEVEL 04\n \n+/* Remove the_original_cwd too */\n+#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08\n+\n /*\n  * Remove path and its contents, recursively. flags is a combination\n  * of the above REMOVE_DIR_* constants. Return 0 on success.\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex cba817ff734..8f299fd3b19 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -282,11 +282,6 @@ test_submodule_removal () {\n \ttest_status=\n \ttest $path_status = dir && test_status=test_must_fail\n \n-\t# Actually, while path_status == dir && test_status=test_must_fail\n-\t# reflect our desired behavior, current behavior is:\n-\tpath_status=missing\n-\ttest_status=\n-\n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442387","messageId":"39b1f3a225ec74a79320503eff04ba47ae4259fd.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:58Z","receivedAt":"2021-11-26T22:45:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince rebase spawns a `checkout` subprocess, make sure we run that from\nthe startup_info->original_cwd directory, so that the checkout process\nknows to protect that directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n sequencer.c          | 3 +++\n t/t2501-cwd-empty.sh | 4 ++--\n 2 files changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex ea96837cde3..b71f7b8a0a6 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n \n \tcmd.git_cmd = 1;\n \n+\tif (startup_info->original_cwd &&\n+\t    !is_absolute_path(startup_info->original_cwd))\n+\t\tcmd.dir = startup_info->original_cwd;\n \tstrvec_push(&cmd.args, \"checkout\");\n \tstrvec_push(&cmd.args, commit);\n \tstrvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be04b673f6a..acf9646ec6e 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -159,11 +159,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rebase reverted\n+\ttest_incidental_dir_removal success git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git rebase fd_conflict\n+\ttest_required_dir_removal success git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442388","messageId":"2c73a09a2e83f08bcb0a5449f48e7e2ca5ac914e.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 09/11] dir: avoid incidentally removing the original_cwd in remove_path()","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:41:00Z","receivedAt":"2021-11-26T22:45:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nModern git often tries to avoid leaving empty directories around when\nremoving files.  Originally, it did not bother.  This behavior started\nwith commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\ndirectories, 2005-11-19), stating the reason simply as:\n\n    When the last file in a directory is removed as the result of a\n    merge, try to rmdir the now-empty directory.\n\nThis was reimplemented in C and renamed to remove_path() in commit\ne1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\ninternal to merge-recursive.\n\nThis trend towards removing leading empty directories continued with\ncommit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\nstated the reasoning as:\n\n    The other question is what to do with leading directories. The old\n    \"git rm\" script didn't do anything, which is somewhat inconsistent.\n    This one will actually clean up directories that have become empty\n    as a result of removing the last file, but maybe we want to have a\n    flag to decide the behaviour?\n\nremove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\nfunction to remove as much as possible of a path, 2008-09-27), because\nit was noted that we had two separate implementations of the same idea\nAND both were buggy.  It described the purpose of the function as\n\n    a function to remove as much as possible of a path\n\nWhy remove as much as possible?  Well, at the time we probably would\nhave said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nBut I don't believe those reasons hold when the empty directory happens\nto be the current working directory we inherited from our parent\nprocess.  Leaving the parent process in a deleted directory can cause\nuser confusion when subsequent processes fail: any git command, for\nexample, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nOther commands may similarly get confused.  Modify remove_path() so that\nthe empty leading directories it also deletes does not include the\ncurrent working directory we inherited from our parent process.  I have\nlooked through every caller of remove_path() in the current codebase to\nmake sure that all should take this change.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                |  3 +++\n dir.h                |  6 +++++-\n t/t2501-cwd-empty.sh | 12 ++++--------\n 3 files changed, 12 insertions(+), 9 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..97d6b71c872 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3327,6 +3327,9 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/dir.h b/dir.h\nindex 83f46c0fb4c..d6a5d03bec2 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n  */\n int remove_dir_recursively(struct strbuf *path, int flag);\n \n-/* tries to remove the path with empty directories along it, ignores ENOENT */\n+/*\n+ * Tries to remove the path, along with leading empty directories so long as\n+ * those empty directories are not startup_info->original_cwd.  Ignores\n+ * ENOENT.\n+ */\n int remove_path(const char *path);\n \n int fspathcmp(const char *a, const char *b);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 6788a0d267f..cba817ff734 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -175,12 +175,12 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rm bar/baz.t\n+\ttest_incidental_dir_removal success git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal failure git apply ../patch\n+\ttest_incidental_dir_removal success git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n@@ -262,12 +262,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n \t) &&\n \n \ttest_path_is_missing a/b/c/tracked &&\n-\t## We would prefer if a/b was still present, though empty, since it\n-\t## was the current working directory\n-\t#test_path_is_dir a/b\n-\t## But the current behavior is that it not only deletes the directory\n-\t## a/b as requested, but also goes and deletes a\n-\ttest_path_is_missing a\n+\ttest_path_is_missing a/b/c &&\n+\ttest_path_is_dir a/b\n '\n \n test_expect_success 'git version continues working from a deleted dir' '\n-- \ngitgitgadget\n\n"},{"id":"442389","messageId":"0110462a19c0ad1f11bbb6449f2ec84fe14c1442.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 08/11] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:40:59Z","receivedAt":"2021-11-26T22:45:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince stash spawns a `clean` subprocess, make sure we run that from the\nstartup_info->original_cwd directory, so that the `clean` processs knows\nto protect that directory.  Also, since the `clean` command might no\nlonger run from the toplevel, pass the ':/' magic pathspec to ensure we\nstill clean from the toplevel.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 5 ++++-\n t/t2501-cwd-empty.sh | 2 +-\n 2 files changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..4d89cc7f969 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1485,8 +1485,11 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !is_absolute_path(startup_info->original_cwd))\n+\t\t\t\tcp.dir = startup_info->original_cwd;\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex acf9646ec6e..6788a0d267f 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -227,7 +227,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n+\ttest_incidental_untracked_dir_removal success \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n\n"},{"id":"442390","messageId":"7eb6281be4b4a1e514719db5c886ab81fe4a5034.1637966463.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v3 11/11] t2501: simplify the tests since we can now assume desired behavior","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-26T22:41:02Z","receivedAt":"2021-11-26T22:45:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe no longer are dealing with a mixture of previous and desired\nbehavior, so simplify the tests a bit.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 105 +++++++++++--------------------------------\n 1 file changed, 27 insertions(+), 78 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 8f299fd3b19..88e27b16e11 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -32,9 +32,6 @@ test_expect_success setup '\n '\n \n test_incidental_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -44,81 +41,49 @@ test_incidental_dir_removal () {\n \t\tcd foo &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif [[ $works == \"success\" ]]; then\n-\t\t\tpwd -P &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! pwd -P &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\tpwd -P &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif [[ $works == \"success\" ]]; then\n-\t\ttest_path_is_dir foo\n-\telse\n-\t\ttest_path_is_missing foo\n-\tfi\n+\ttest_path_is_dir foo\n }\n \n test_required_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \tgit checkout df_conflict^{commit} &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We'd like for the command to fail (much as it would if there\n-\t\t# was an untracked file there), and for pwd & git status to\n-\t\t# succeed afterwards.  But test for existing rather than\n-\t\t# desired behavior.\n-\t\tif [[ $works == \"success\" ]]; then\n-\t\t\ttest_must_fail \"$@\" 2>../error &&\n-\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n-\t\t\tpwd -P &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t\"$@\" &&\n-\t\t\t! pwd -P &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\t\tpwd -P &&\n+\t\tgit status --porcelain\n \t) &&\n \n-\t# Although we want dirORfile to be present, test for existing rather\n-\t# than desired behavior.\n-\tif [[ $works == \"success\" ]]; then\n-\t\ttest_path_is_dir dirORfile\n-\telse\n-\t\ttest_path_is_file dirORfile\n-\tfi\n+\ttest_path_is_dir dirORfile\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git checkout init\n+\ttest_incidental_dir_removal git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git checkout fd_conflict\n+\ttest_required_dir_removal git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git reset --hard init\n+\ttest_incidental_dir_removal git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git reset --hard fd_conflict\n+\ttest_required_dir_removal git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git merge reverted\n+\ttest_incidental_dir_removal git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -147,46 +112,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git merge fd_conflict\n+\ttest_required_dir_removal git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git cherry-pick reverted\n+\ttest_incidental_dir_removal git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git cherry-pick fd_conflict\n+\ttest_required_dir_removal git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rebase reverted\n+\ttest_incidental_dir_removal git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git rebase fd_conflict\n+\ttest_required_dir_removal git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git revert HEAD\n+\ttest_incidental_dir_removal git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git revert undo_fd_conflict\n+\ttest_required_dir_removal git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rm bar/baz.t\n+\ttest_incidental_dir_removal git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal success git apply ../patch\n+\ttest_incidental_dir_removal git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -198,36 +160,23 @@ test_incidental_untracked_dir_removal () {\n \t\tcd untracked &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif [[ $works == \"success\" ]]; then\n-\t\t\tpwd -P &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! pwd -P &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\tpwd -P &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing empty &&\n \ttest_path_is_missing untracked/random &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif [[ $works == \"success\" ]]; then\n-\t\ttest_path_is_dir untracked\n-\telse\n-\t\ttest_path_is_missing untracked\n-\tfi\n+\ttest_path_is_dir untracked\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n \tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n"},{"id":"442398","messageId":"211127.864k7xj38p.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"4b0044656b0ebe81db60f605a184948f728c4b9d.1637966463.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 01/11] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-27T10:32:17Z","receivedAt":"2021-11-27T10:37:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n\n> +\t\t# Although we want pwd & git status to pass, test for existing\n> +\t\t# rather than desired behavior.\n> +\t\tif [[ $works == \"success\" ]]; then\n\nWasn't \"[[\" bash-specific or something? In any case a more regular \"if\ntest \"$works\" = \"success\" would work here.\n\n> +\t\t\tpwd -P &&\n\nI wonder if this is doing what we want everywhere cf. 482e1488a9b\n(t0001: fix broken not-quite getcwd(3) test in bed67874e2, 2021-07-30),\nbut haven't looked much/thought about it.\n\n> +test_expect_success 'checkout does not clean cwd incidentally' '\n> +\ttest_incidental_dir_removal failure git checkout init\n> +'\n> +\n> +test_expect_success 'checkout fails if cwd needs to be removed' '\n> +\ttest_required_dir_removal failure git checkout fd_conflict\n> +'\n> +\n> +test_expect_success 'reset --hard does not clean cwd incidentally' '\n> +\ttest_incidental_dir_removal failure git reset --hard init\n> +'\n> +\n> +test_expect_success 'reset --hard fails if cwd needs to be removed' '\n> +\ttest_required_dir_removal failure git reset --hard fd_conflict\n> +'\n> +\n> +test_expect_success 'merge does not clean cwd incidentally' '\n> +\ttest_incidental_dir_removal failure git merge reverted\n> +'\n\nThis testing the current behavior (and below) looks much better, thanks!\n"},{"id":"442399","messageId":"211127.86zgpphohi.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"200ddece05d3d0599b16897ff9533cdfb3324b0c.1637966463.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-27T10:35:08Z","receivedAt":"2021-11-27T10:41:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n> [...]\n> +\t/*\n> +\t * Get our worktree; we only protect the current working directory\n> +\t * if it's in the worktree.\n> +\t */\n> +\tworktree = get_git_work_tree();\n> +\tif (!worktree)\n> +\t\tgoto no_prevention_needed;\n> +\n> +\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n> +\tif (offset >= 0) {\n\nNit: Easier to read as:\n\n    offset = [...]\n    if (offset < 0)\n    \treturn;\n\nI.e. the reader can skip that whole \"offset >= 0\" block and anything\nafter if it <0, which also reduces the indentation. We run into the\n\"return' below.\n\n> +\t\t/*\n> +\t\t * If startup_info->original_cwd == worktree, that is already\n> +\t\t * protected and we don't need original_cwd as a secondary\n> +\t\t * protection measure.\n> +\t\t */\n> +\t\tif (!*(startup_info->original_cwd + offset))\n> +\t\t\tgoto no_prevention_needed;\n> +\n> +\t\t/*\n> +\t\t * original_cwd was inside worktree; precompose it just as\n> +\t\t * we do prefix so that built up paths will match\n> +\t\t */\n> +\t\tstartup_info->original_cwd = \\\n\nTIL you can use backslashes like that in C outside of macros, but it's\nnot needed here, better without?\n\n> +\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n> +\t\t\t\t\t\t    + offset);\n> +\t}\n> +\treturn;\n\n"},{"id":"442401","messageId":"211127.86v90dhf5g.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"200ddece05d3d0599b16897ff9533cdfb3324b0c.1637966463.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-27T10:40:04Z","receivedAt":"2021-11-27T14:02:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n\n[Just some more \"I haven't really looked at this in all that much\ndetail\" commentary, so maybe it's stupid, sorry]\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Removing the current working directory causes all subsequent git\n> commands run from that directory to get confused and fail with a message\n> about being unable to read the current working directory:\n>\n>     $ git status\n>     fatal: Unable to read current working directory: No such file or directory\n>\n> Non-git commands likely have similar warnings or even errors, e.g.\n>\n>     $ bash -c 'echo hello'\n>     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n>     hello\n\nIs that really realistic? Any \"normal\" command would use \"pwd\" or look\nat $PWD, both of which \"work\", this error is only because we're starting\na new shell.\n\nI wonder if it was just because you ran into our bin-wrappers edge case,\nbut that should be really obscure for any real users.\n\n> This confuses end users, particularly since the command they get the\n> error from is not the one that caused the problem; the problem came from\n> the side-effect of some previous command.\n>\n> We would like to avoid removing the current working directory of our\n> parent process; towards this end, introduce a new variable,\n> startup_info->original_cwd, that tracks the current working directory\n> that we inherited from our parent process.  For convenience of later\n> comparisons, we prefer that this new variable store a path relative to\n> the toplevel working directory (thus much like 'prefix'), except without\n> the trailing slash.\n\nI'm still not clear at all on why we need a \"original_cwd\" at all then\nas opposed to just using \"prefix\" (or adding \"the_prefix\" if passing it\ndown is painful). I.e. we discover our relative path, we resolve the\nrelative path to the root, can't we use that as the \"don't remove our\nCWD\" guard?\n\nDoes our prefix change at some point, then maybe \"orig_prefix\" would\nmake more sense?\n\nThis is with the context that I haven't dug into your code, so maybe\nthere's some obvious reason I'm missing...\n"},{"id":"442402","messageId":"CABPp-BFbmnvtUTW30KjWN6tQECjN=ErKC64oXrCLrb1OmXOZyQ@mail.gmail.com","threadId":"56952","inReplyTo":"211127.86zgpphohi.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-27T17:05:17Z","receivedAt":"2021-11-27T17:07:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sat, Nov 27, 2021 at 2:39 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> > [...]\n> > +     /*\n> > +      * Get our worktree; we only protect the current working directory\n> > +      * if it's in the worktree.\n> > +      */\n> > +     worktree = get_git_work_tree();\n> > +     if (!worktree)\n> > +             goto no_prevention_needed;\n> > +\n> > +     offset = dir_inside_of(startup_info->original_cwd, worktree);\n> > +     if (offset >= 0) {\n>\n> Nit: Easier to read as:\n>\n>     offset = [...]\n>     if (offset < 0)\n>         return;\n>\n> I.e. the reader can skip that whole \"offset >= 0\" block and anything\n> after if it <0, which also reduces the indentation. We run into the\n> \"return' below.\n\nWhoops; the return should be inside the braces.  I'll fix.\n\n> > +             /*\n> > +              * If startup_info->original_cwd == worktree, that is already\n> > +              * protected and we don't need original_cwd as a secondary\n> > +              * protection measure.\n> > +              */\n> > +             if (!*(startup_info->original_cwd + offset))\n> > +                     goto no_prevention_needed;\n> > +\n> > +             /*\n> > +              * original_cwd was inside worktree; precompose it just as\n> > +              * we do prefix so that built up paths will match\n> > +              */\n> > +             startup_info->original_cwd = \\\n>\n> TIL you can use backslashes like that in C outside of macros, but it's\n> not needed here, better without?\n\nLine is too long without it, so better with it.\n\n>\n> > +                     precompose_string_if_needed(startup_info->original_cwd\n> > +                                                 + offset);\n> > +     }\n> > +     return;\n"},{"id":"442403","messageId":"CABPp-BFuKBht2yZ+tYkG+C4Rpek6dLT0gx6pZNH4rXqv0UFf3Q@mail.gmail.com","threadId":"56952","inReplyTo":"211127.86v90dhf5g.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-27T18:31:50Z","receivedAt":"2021-11-27T18:34:35Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n>\n> [Just some more \"I haven't really looked at this in all that much\n> detail\" commentary, so maybe it's stupid, sorry]\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Removing the current working directory causes all subsequent git\n> > commands run from that directory to get confused and fail with a message\n> > about being unable to read the current working directory:\n> >\n> >     $ git status\n> >     fatal: Unable to read current working directory: No such file or directory\n> >\n> > Non-git commands likely have similar warnings or even errors, e.g.\n> >\n> >     $ bash -c 'echo hello'\n> >     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n> >     hello\n>\n> Is that really realistic?\n\nI have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my\nFedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu\nworkstation at $DAYJOB.  That's not even considering stuff in other\ndirectories.  Note that I didn't place any of those scripts in\n/usr/bin/; they came from the distribution and/or corporate packages\nfrom others.  Any invocation of bash will see the above \"error\"\nmessage.  Granted, bash calls it an 'error' but continues anyway, but\nI thought it showed that there were clearly programs besides git where\nusers could run into problems.\n\n> Any \"normal\" command would use \"pwd\" or look\n> at $PWD, both of which \"work\", this error is only because we're starting\n> a new shell.\n\n<grin>\n\nYeah, good point.  Who would use an unusual program like git?  Or a\nshell script?  Or a java program?  Or emacs?  Or other programs like\nthem?  Besides, git, bash, java, and emacs are all relatively young\ncommands with small development teams.  Maybe we should just rely on\nusers only using commands that get the pwd/$PWD detail right; perhaps\nlimiting to commands that are more mature and have bigger development\nteams than those four.\n\nSilly users.\n\n;-)\n\nSorry, couldn't resist a little friendly teasing.\n\n\nHowever, for \"pwd\"...by \"work\", do you mean \"doesn't necessarily\nwork\"?  On my machine:\n\n$ mkdir gone\n$ cd gone\n$ rmdir ../gone\n$ pwd -P\npwd: error retrieving current directory: getcwd: cannot access parent\ndirectories: No such file or directory\n$ pwd\npwd: error retrieving current directory: getcwd: cannot access parent\ndirectories: No such file or directory\n$ echo $PWD\n/home/newren/floss/git/gone\n\nIf I do not run `pwd -P` first then a plain `pwd` works.  But your\nadvice to use `pwd` seems misguided for programs, since they'll hit\nthis problem if users have run a previous `pwd -P`.  (The $PWD trick\nwould have worked, as shown above)\n\n> I wonder if it was just because you ran into our bin-wrappers edge case,\n> but that should be really obscure for any real users.\n\nYes, I agree our bin-wrappers is a really obscure edge case not worth\nconsidering.  I don't see how you jump from there to painting all\nshell scripts combined with the same brush, though.\n\n> > This confuses end users, particularly since the command they get the\n> > error from is not the one that caused the problem; the problem came from\n> > the side-effect of some previous command.\n> >\n> > We would like to avoid removing the current working directory of our\n> > parent process; towards this end, introduce a new variable,\n> > startup_info->original_cwd, that tracks the current working directory\n> > that we inherited from our parent process.  For convenience of later\n> > comparisons, we prefer that this new variable store a path relative to\n> > the toplevel working directory (thus much like 'prefix'), except without\n> > the trailing slash.\n>\n> I'm still not clear at all on why we need a \"original_cwd\" at all then\n> as opposed to just using \"prefix\" (or adding \"the_prefix\" if passing it\n> down is painful). I.e. we discover our relative path, we resolve the\n> relative path to the root, can't we use that as the \"don't remove our\n> CWD\" guard?\n>\n> Does our prefix change at some point, then maybe \"orig_prefix\" would\n> make more sense?\n\nNo; see the code comment in the same patch:\n\n+       /*\n+        * startup_info->original_cwd points to the current working\n+        * directory we inherited from our parent process, which is a\n+        * directory we want to avoid removing.\n+        *\n+        * For convience, we would like to have the path relative to the\n+        * worktree instead of an absolute path.\n+        *\n+        * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+        * but differs in two ways:\n+        *   - prefix has a trailing '/'\n+        *   - if the user passes '-C' to git, that modifies the prefix but\n+        *     not startup_info->original_cwd.\n+        */\n\nIt's never equal to prefix, even though it's usually semantically\nreferring to the same directory.  However, even if it weren't for the\ntrailing slash issue, the -C case means it is not appropriate to think\nof it as \"orig_prefix\" either.\n"},{"id":"442404","messageId":"CABPp-BEXghA4rZCx10hCWWNjtPLkMWXAXU6YNbA4JMZ8WmVPfA@mail.gmail.com","threadId":"56952","inReplyTo":"211127.864k7xj38p.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-27T19:16:33Z","receivedAt":"2021-11-27T19:18:52Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sat, Nov 27, 2021 at 2:35 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n>\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > +             # Although we want pwd & git status to pass, test for existing\n> > +             # rather than desired behavior.\n> > +             if [[ $works == \"success\" ]]; then\n>\n> Wasn't \"[[\" bash-specific or something? In any case a more regular \"if\n> test \"$works\" = \"success\" would work here.\n\nThanks; will fix.\n\n> > +                     pwd -P &&\n>\n> I wonder if this is doing what we want everywhere cf. 482e1488a9b\n> (t0001: fix broken not-quite getcwd(3) test in bed67874e2, 2021-07-30),\n> but haven't looked much/thought about it.\n\nI can change to `test-tool getcwd` for the obscure platforms; thanks\nfor the tip.\n\n> > +test_expect_success 'checkout does not clean cwd incidentally' '\n> > +     test_incidental_dir_removal failure git checkout init\n> > +'\n> > +\n> > +test_expect_success 'checkout fails if cwd needs to be removed' '\n> > +     test_required_dir_removal failure git checkout fd_conflict\n> > +'\n> > +\n> > +test_expect_success 'reset --hard does not clean cwd incidentally' '\n> > +     test_incidental_dir_removal failure git reset --hard init\n> > +'\n> > +\n> > +test_expect_success 'reset --hard fails if cwd needs to be removed' '\n> > +     test_required_dir_removal failure git reset --hard fd_conflict\n> > +'\n> > +\n> > +test_expect_success 'merge does not clean cwd incidentally' '\n> > +     test_incidental_dir_removal failure git merge reverted\n> > +'\n>\n> This testing the current behavior (and below) looks much better, thanks!\n\n:-)\n"},{"id":"442445","messageId":"48fced3c-1fcb-d5cd-cec0-b9e58be40259@gmail.com","threadId":"56952","inReplyTo":"f6129a8ac9c3d052fb7fb508a62d4eedb8d9ed57.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T14:05:55Z","receivedAt":"2021-11-29T15:57:22Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n\n> @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n>  \n>  \ttrace_command_performance(argv);\n>  \n> +\tstartup_info->original_cwd = xgetcwd();\n> +\n\nI see this initial assignment in cmd_main()...\n\n> +static void setup_original_cwd(void)\n> +{\n> +\tstruct strbuf tmp = STRBUF_INIT;\n> +\tconst char *worktree = NULL;\n> +\tint offset = -1;\n> +\n> +\t/*\n> +\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n> +\t * we're an auxiliary tool like git-remote-http or test-tool.\n> +\t */\n> +\tif (!startup_info->original_cwd)\n> +\t\treturn;\n\n...which is assumed to be run before this method was called...\n\n> @@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>  \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n>  \t}\n>  \n> +\tsetup_original_cwd();\n\n...here in setup_git_directory_gently().\n\nWhy do we need that assignment in cmd_main()? Could we instead\nlet setup_original_cwd() do the initial assignment? Or is it\npossible that a chdir has happened already before this point?\n\nThanks,\n-Stolee\n"},{"id":"442446","messageId":"ac7e7241-d852-f0e7-d9c1-053adb242fee@gmail.com","threadId":"56952","inReplyTo":"e74975e83cc7a11b8f0378d59a8c2c4a97d3aa50.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 3/9] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T14:10:33Z","receivedAt":"2021-11-29T16:08:16Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n\n> @@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n>  \t/* ERROR_NOT_UPTODATE_DIR */\n>  \t\"Updating '%s' would lose untracked files in it\",\n>  \n> +\t/* ERROR_CWD_IN_THE_WAY */\n> +\t\"Refusing to remove '%s' since it is the current working directory.\",\n> +\n>  \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n>  \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n\nYour new message includes a hard stop (\".\") which is non-standard. I\nsee that the message after yours has one, but the preceding one does\nnot. Since the file you are in is not consistent, I would choose to\ndrop the hard stop here.\n\nThanks,\n-Stolee\n"},{"id":"442447","messageId":"f65473df-4cb0-afdb-70e3-ee7685e6d817@gmail.com","threadId":"56952","inReplyTo":"e06806e3a3266fa6fdc973fcd7b479bd395c3023.1637829556.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 4/9] unpack-trees: add special cwd handling","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T14:14:19Z","receivedAt":"2021-11-29T16:17:20Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> When running commands such as `git reset --hard` from a subdirectory, if\n> that subdirectory is in the way of adding needed files, bail with an\n> error message.\n...\n> -test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n> +test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>  \tgit checkout foo/bar/baz &&\n>  \ttest_when_finished \"git clean -fdx\" &&\n\nThis test doesn't include any condition on the final state of\nthe worktree. I could imagine that an implementation could be\nwriting to the worktree incrementally and then bail halfway\nthrough. A clean \"git status\" would give us some confidence that\nthe worktree and index are in a good state after the command\nfails.\n\n> +\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n> +\t\t/* Avoid nuking startup_info->original_cwd... */\n> +\t\tif (startup_info->original_cwd &&\n> +\t\t    !strcmp(startup_info->original_cwd, ce->name))\n> +\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n> +\t\t\t\t\t\t ce->name);\n> +\t\t/* ...but nuke anything else. */\n> +\t\treturn 0;\n> +\t}\n> +\n\nThis implementation appears to do the right thing in the case\nI'm describing: we check which entries would need to change and\nwe reject them before doing any real work.\n\nThanks,\n-Stolee\n"},{"id":"442452","messageId":"xmqqwnkq3llj.fsf@gitster.g","threadId":"56952","inReplyTo":"48fced3c-1fcb-d5cd-cec0-b9e58be40259@gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-29T17:42:00Z","receivedAt":"2021-11-29T17:44:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n>> @@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>>  \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n>>  \t}\n>>  \n>> +\tsetup_original_cwd();\n>\n> ...here in setup_git_directory_gently().\n>\n> Why do we need that assignment in cmd_main()? Could we instead\n> let setup_original_cwd() do the initial assignment? Or is it\n> possible that a chdir has happened already before this point?\n\nSince setup_git_directory() is used to figure out the prefix, which\nis relative path to the directory, in which the command was started,\nviewed from the top-level of the working tree, it would be a bug if\nthe caller did any chdir(2) before it called the function.\n\nI didn't look at or thought about how chdir(2) done inside this\nfunction affects what setup_original_cwd() wants to do, though.\n\nThanks.\n"},{"id":"442487","messageId":"CABPp-BHStJ2x53cffJPb7miaxL2cKr0usgK13HxNDyYMQv11_g@mail.gmail.com","threadId":"56952","inReplyTo":"48fced3c-1fcb-d5cd-cec0-b9e58be40259@gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-29T17:18:31Z","receivedAt":"2021-11-29T20:41:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 6:05 AM Derrick Stolee <stolee@gmail.com> wrote:\n>\n> On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n> >\n> >       trace_command_performance(argv);\n> >\n> > +     startup_info->original_cwd = xgetcwd();\n> > +\n>\n> I see this initial assignment in cmd_main()...\n\nIt looks like you accidentally responded to v2 when there's a v3\n(something I occasionally do too).  v3 changes this to put it in\ncommon-main instead of here, as suggested by Ævar, but to answer the\nquestion...\n\n> > +static void setup_original_cwd(void)\n> > +{\n> > +     struct strbuf tmp = STRBUF_INIT;\n> > +     const char *worktree = NULL;\n> > +     int offset = -1;\n> > +\n> > +     /*\n> > +      * startup_info->original_cwd wass set early on in cmd_main(), unless\n> > +      * we're an auxiliary tool like git-remote-http or test-tool.\n> > +      */\n> > +     if (!startup_info->original_cwd)\n> > +             return;\n>\n> ...which is assumed to be run before this method was called...\n>\n> > @@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n> >               setenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n> >       }\n> >\n> > +     setup_original_cwd();\n>\n> ...here in setup_git_directory_gently().\n>\n> Why do we need that assignment in cmd_main()? Could we instead\n> let setup_original_cwd() do the initial assignment? Or is it\n> possible that a chdir has happened already before this point?\n\nIn v1, I made that mistake.  Then I realized that when users pass the\n-C option to git, there is a chdir() call immediately upon parsing of\nthe -C option.  So I had to move the strbuf_getcwd() call earlier.\n"},{"id":"442489","messageId":"CABPp-BHsH0TpsGziLBQW-i1W_XicxyxJUtJCTdu6AwdZvzkmbQ@mail.gmail.com","threadId":"56952","inReplyTo":"ac7e7241-d852-f0e7-d9c1-053adb242fee@gmail.com","subject":"Re: [PATCH v2 3/9] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-29T17:26:34Z","receivedAt":"2021-11-29T20:47:10Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 6:10 AM Derrick Stolee <stolee@gmail.com> wrote:\n>\n> On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n>\n> > @@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n> >       /* ERROR_NOT_UPTODATE_DIR */\n> >       \"Updating '%s' would lose untracked files in it\",\n> >\n> > +     /* ERROR_CWD_IN_THE_WAY */\n> > +     \"Refusing to remove '%s' since it is the current working directory.\",\n> > +\n> >       /* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n> >       \"Untracked working tree file '%s' would be overwritten by merge.\",\n>\n> Your new message includes a hard stop (\".\") which is non-standard. I\n> see that the message after yours has one, but the preceding one does\n> not. Since the file you are in is not consistent, I would choose to\n> drop the hard stop here.\n\nSeems pretty reasonable, but 9 out of 10 of the messages in that list\ncontain the hard stop.  True, that's still inconsistent, but adding\nthe hard stop feels more internally consistent.  I think I'd rather\nhave someone else post a subsequent cleanup to remove the hard stops\nand lowercase the first letter and whatever else needs to be changed\nwith these messages.\n"},{"id":"442514","messageId":"211129.868rx6g0e0.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"CABPp-BFuKBht2yZ+tYkG+C4Rpek6dLT0gx6pZNH4rXqv0UFf3Q@mail.gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-28T18:04:25Z","receivedAt":"2021-11-29T22:24:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Sat, Nov 27 2021, Elijah Newren wrote:\n\n> On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason\n> <avarab@gmail.com> wrote:\n>>\n>> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n>>\n>> [Just some more \"I haven't really looked at this in all that much\n>> detail\" commentary, so maybe it's stupid, sorry]\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>> >\n>> > Removing the current working directory causes all subsequent git\n>> > commands run from that directory to get confused and fail with a message\n>> > about being unable to read the current working directory:\n>> >\n>> >     $ git status\n>> >     fatal: Unable to read current working directory: No such file or directory\n>> >\n>> > Non-git commands likely have similar warnings or even errors, e.g.\n>> >\n>> >     $ bash -c 'echo hello'\n>> >     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n>> >     hello\n>>\n>> Is that really realistic?\n>\n> I have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my\n> Fedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu\n> workstation at $DAYJOB.  That's not even considering stuff in other\n> directories.  Note that I didn't place any of those scripts in\n> /usr/bin/; they came from the distribution and/or corporate packages\n> from others.  Any invocation of bash will see the above \"error\"\n> message.  Granted, bash calls it an 'error' but continues anyway, but\n> I thought it showed that there were clearly programs besides git where\n> users could run into problems.\n>\n>> Any \"normal\" command would use \"pwd\" or look\n>> at $PWD, both of which \"work\", this error is only because we're starting\n>> a new shell.\n>\n> <grin>\n>\n> Yeah, good point.  Who would use an unusual program like git?  Or a\n> shell script?  Or a java program?  Or emacs?  Or other programs like\n> them?  Besides, git, bash, java, and emacs are all relatively young\n> commands with small development teams.  Maybe we should just rely on\n> users only using commands that get the pwd/$PWD detail right; perhaps\n> limiting to commands that are more mature and have bigger development\n> teams than those four.\n>\n> Silly users.\n>\n> ;-)\n>\n> Sorry, couldn't resist a little friendly teasing.\n>\n>\n> However, for \"pwd\"...by \"work\", do you mean \"doesn't necessarily\n> work\"?  On my machine:\n>\n> $ mkdir gone\n> $ cd gone\n> $ rmdir ../gone\n> $ pwd -P\n> pwd: error retrieving current directory: getcwd: cannot access parent\n> directories: No such file or directory\n> $ pwd\n> pwd: error retrieving current directory: getcwd: cannot access parent\n> directories: No such file or directory\n> $ echo $PWD\n> /home/newren/floss/git/gone\n>\n> If I do not run `pwd -P` first then a plain `pwd` works.  But your\n> advice to use `pwd` seems misguided for programs, since they'll hit\n> this problem if users have run a previous `pwd -P`.  (The $PWD trick\n> would have worked, as shown above)\n>\n>> I wonder if it was just because you ran into our bin-wrappers edge case,\n>> but that should be really obscure for any real users.\n\nFor some reason I was under the misimpression that the \"#!/bin/bash\"\npart of the bin-wrappers and other scripts somehow immunized them from\nthe $PWD/\"pwd\" reset, and it was only the programs they invoked (like\ngit in the bin-wrappers) that didn't get the values passed along.\n\nBut that's clearly incorrect as you demonstrate above, so the only thing\nthat'll work OK (seemingly) is running \"pwd\" (but not \"pwd -P\"), or\nlooking at $PWD in your terminal shell itself.\n\nInvoking non-POSIX shell programs \"works\" in that they can use the same\ntrick, after the dance of \"mkdir x && cd x && rm -rf ..x\" e.g. Perl\nsays:\n    \n    $ perl -MCwd=getcwd -wE 'say $ENV{PWD}; say getcwd'\n    /home/avar/g/git/x\n    Use of uninitialized value in say at -e line 1\n\nThis \"pwd -P\" behavior isn't just something weird in your shell & mine,\nit semse to be mandated by POSIX:\nhttps://pubs.opengroup.org/onlinepubs/007904875/utilities/pwd.html\n\nAll of which is to say I'm much more sympathetic to this approach\nnow. I.e. it seemed like purely a way to work around fixable breakages\nin our own and other programs. Well, I guess \"don't use shellscript if\nyou care about this edge case\" is a \"fix\", but not a trivial one.\n\nI do still think a better starting point would be fixing the setup.c\ndying in our own code, and see where that leaves us, but up to you\netc. I'm not going to work on it any time soon.\n\n> Yes, I agree our bin-wrappers is a really obscure edge case not worth\n> considering.  I don't see how you jump from there to painting all\n> shell scripts combined with the same brush, though.\n\n*nod*\n\n>> > This confuses end users, particularly since the command they get the\n>> > error from is not the one that caused the problem; the problem came from\n>> > the side-effect of some previous command.\n>> >\n>> > We would like to avoid removing the current working directory of our\n>> > parent process; towards this end, introduce a new variable,\n>> > startup_info->original_cwd, that tracks the current working directory\n>> > that we inherited from our parent process.  For convenience of later\n>> > comparisons, we prefer that this new variable store a path relative to\n>> > the toplevel working directory (thus much like 'prefix'), except without\n>> > the trailing slash.\n>>\n>> I'm still not clear at all on why we need a \"original_cwd\" at all then\n>> as opposed to just using \"prefix\" (or adding \"the_prefix\" if passing it\n>> down is painful). I.e. we discover our relative path, we resolve the\n>> relative path to the root, can't we use that as the \"don't remove our\n>> CWD\" guard?\n>>\n>> Does our prefix change at some point, then maybe \"orig_prefix\" would\n>> make more sense?\n>\n> No; see the code comment in the same patch:\n>\n> +       /*\n> +        * startup_info->original_cwd points to the current working\n> +        * directory we inherited from our parent process, which is a\n> +        * directory we want to avoid removing.\n> +        *\n> +        * For convience, we would like to have the path relative to the\n> +        * worktree instead of an absolute path.\n> +        *\n> +        * Yes, startup_info->original_cwd is usually the same as 'prefix',\n> +        * but differs in two ways:\n> +        *   - prefix has a trailing '/'\n> +        *   - if the user passes '-C' to git, that modifies the prefix but\n> +        *     not startup_info->original_cwd.\n> +        */\n>\n> It's never equal to prefix, even though it's usually semantically\n> referring to the same directory.  However, even if it weren't for the\n> trailing slash issue, the -C case means it is not appropriate to think\n> of it as \"orig_prefix\" either.\n\nAh, with -C of e.g. t/helper we'll first chdir(t/helper), and then run\nthe usual setup.c dance to find that we need to chdir() again to the\n(equivalent of) \"../../\". But our prefix stays at \"t/helper\".\n\nI'm a bit confused about the trailing slash case, isn't the prefix\nalways going to point to a directory? Why would t/helper v.s. t/helper/\nmatter?\n\nI think it won't matter for rmdir(2) et al, but maybe I'm wrong.\n\nWhat got me confused about the \"prefix\" v.s. \"original_cwd\" is that I\nwas assuming they'd be the same. The commentary on setup_git_directory()\nsays as much, i.e.:\n\n    Returns the \"prefix\", a path to the current working directory\n    relative to the work tree root, or NULL,\n\nBut of course we know that's a white lie, it's not the $PWD/getcwd(). So\nyou're only trying to save the user in cases of e.g. (in t/helper):\n\n    git rm ../helper\n\nIf they actually run it while in t/helper, but would like to explicitly\nomit the case of (at the top-level):\n\n    git -C t/helper rm ../helper\n\nThat's fair enough I guess. I'd just assumed those cases would be\ntreated the same way.\n\nEven in that case, I can't think of a case where this \"original_cwd\"\nwouldn't be made redundant by some boolean flag to accompany \"prefix\" to\nindicate that we've chdir'd out of the prefix (or not).\n"},{"id":"442534","messageId":"39e2bc15-298c-0932-109b-e92da3979dc2@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T17:57:46Z","receivedAt":"2021-11-29T22:29:47Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:\n> Traditionally, if folks run git commands such as checkout or rebase from a\n> subdirectory, that git command could remove their current working directory\n> and result in subsequent git and non-git commands either getting confused or\n> printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> working directory: No such file or directory\"). Many commands either\n> silently avoid removing directories that are not empty (i.e. those that have\n> untracked or modified files in them)[1], or show an error and abort,\n> depending on which is more appropriate for the command in question. With\n> this series, we augment the reasons to avoid removing directories to include\n> not just has-untracked-or-modified-files, but also to avoid removing the\n> original_cwd as well.\n\nAs I was reviewing v2, this version popped up in my inbox. Sorry about that.\n\nThe only actionable comment from my review of v2 was the addition of a check\nthat the worktree is in the expected state after commands are aborted due to\ntrying to remove the current working directory. Your suggested\n\n\tgit diff --exit-code HEAD\n\nshould work for this. I might even add a \"git rev-parse HEAD\" to make sure\nwe are on the right ref, but that's perhaps too specific to something like\n'git reset --hard <branch>'.\n\n> Changes since v2:\n> \n>  * the series is now only about the working tree. So if the original cwd is\n>    outside the worktree (or we're in a bare repo), then the new code is a\n>    no-op.\n>  * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n>    xgetcwd())\n>  * modified the initial tests to show both expected and desired behavior.\n>    subsequent patches fix the tests. One new patch added at the end which\n>    simplifies the tests to only check for desired behavior.\n>  * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n>    that is already protected and we don't need secondary protection for it.\n>    This simplified some other codepaths so we don't have to check for\n>    startup_info->original_cwd == \"\".\n>  * clarified some commit messages\n\nLooking at these changes I like all of them.\n\n> Range-diff vs v2:\n> \n>   1:  38a120f5c03 !  1:  4b0044656b0 t2501: add various tests for removing the current working directory\n\nI like this new test structure. Using test_expect_success to document existing\nbehavior is a good strategy.\n\n>   2:  f6129a8ac9c !  2:  200ddece05d setup: introduce startup_info->original_cwd>   3:  e74975e83cc !  3:  68ae90546fe unpack-trees: refuse to remove startup_info->original_cwd\n>   4:  e06806e3a32 !  4:  1bb8905900c unpack-trees: add special cwd handling\n>   5:  46728f74ea1 !  5:  8a33d74e7cf symlinks: do not include startup_info->original_cwd in dir removal\n>   6:  01ce9444dae !  6:  11e4ec881bb clean: do not attempt to remove startup_info->original_cwd\nThese changes looked good.\n\n>   -:  ----------- >  7:  39b1f3a225e rebase: do not attempt to remove startup_info->original_cwd\n\nI had a small comment on this one, only because I think there is a condition\nin your 'if' statement that is either unhelpful or is hiding something.\n\n>   7:  edec0894ca2 !  8:  0110462a19c stash: do not attempt to remove startup_info->original_cwd\n...\n>        \t\t\tcp.git_cmd = 1;\n>       +\t\t\tif (startup_info->original_cwd &&\n>      -+\t\t\t    *startup_info->original_cwd &&\n>       +\t\t\t    !is_absolute_path(startup_info->original_cwd))\n>       +\t\t\t\tcp.dir = startup_info->original_cwd;\n\nAnd here is a similar use of is_absolute_path() that could be resolved the\nsame way as whatever you choose for the v3 patch 7.\n\n>   8:  1815f18592b !  9:  2c73a09a2e8 dir: avoid incidentally removing the original_cwd in remove_path()\n>   9:  adaad7aeaac ! 10:  d4e50b4053d dir: new flag to remove_dir_recurse() to spare the original_cwd\n>   -:  ----------- > 11:  7eb6281be4b t2501: simplify the tests since we can now assume desired behavior\n\nThis last test cleanup is good to have.\n\nThanks,\n-Stolee\n"},{"id":"442537","messageId":"CABPp-BFr-U1kaqaj9HR+A=tRt0F_btcbDp89xeoZRdXewM4a2w@mail.gmail.com","threadId":"56952","inReplyTo":"7d90bf61-59c6-045e-1987-81d50454c260@gmail.com","subject":"Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-29T19:22:59Z","receivedAt":"2021-11-29T22:33:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 9:50 AM Derrick Stolee <stolee@gmail.com> wrote:\n>\n> On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Since rebase spawns a `checkout` subprocess, make sure we run that from\n> > the startup_info->original_cwd directory, so that the checkout process\n> > knows to protect that directory.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  sequencer.c          | 3 +++\n> >  t/t2501-cwd-empty.sh | 4 ++--\n> >  2 files changed, 5 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/sequencer.c b/sequencer.c\n> > index ea96837cde3..b71f7b8a0a6 100644\n> > --- a/sequencer.c\n> > +++ b/sequencer.c\n> > @@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n> >\n> >       cmd.git_cmd = 1;\n> >\n> > +     if (startup_info->original_cwd &&\n> > +         !is_absolute_path(startup_info->original_cwd))\n> > +             cmd.dir = startup_info->original_cwd;\n>\n> I was initially confused by the \"!is_absolute_path()\" because\n> it seemed to me like it would be natural to store an absolute\n> path there, but I see this comment in patch 2:\n>\n> +        * For convience, we would like to have the path relative to the\n> +        * worktree instead of an absolute path.\n>\n> So it seems that we won't store it as an absolute path. Is\n> there any value in this condition, then?\n\nGood catch.  This is leftover from an earlier round; I'll remove it.\nFrom stash too.\n\n> This assignment of cmd.dir to the relative path has a lot\n> of baked-in knowledge of this variable _and_ the current\n> state (Git chdir()'d to the root of the worktree). If the\n> path is always relative, then it should be a BUG() if we\n> see an absolute path. Also, it seems like we would want\n> cmd.dir to be a concatenation of the worktree root and the\n> original_cwd.\n>\n> Or perhaps I'm being overly cautious and this could all be\n> resolved with a comment about the expected state of the\n> working directory and original_cwd. The tests will catch if\n> any of those expectations change.\n\nYeah, with the extra condition in there I should have added a good\nexplanation about why I had it in there.  But I think a simple\n\nif (startup_info->original_cwd)\n    cmd.dir = startup_info->original_cwd;\n\nshould be good enough?  Or do you still want a comment for that?\n"},{"id":"442542","messageId":"7d90bf61-59c6-045e-1987-81d50454c260@gmail.com","threadId":"56952","inReplyTo":"39b1f3a225ec74a79320503eff04ba47ae4259fd.1637966463.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T17:50:55Z","receivedAt":"2021-11-29T22:37:11Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:\n> From: Elijah Newren <newren@gmail.com>\n> \n> Since rebase spawns a `checkout` subprocess, make sure we run that from\n> the startup_info->original_cwd directory, so that the checkout process\n> knows to protect that directory.\n> \n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  sequencer.c          | 3 +++\n>  t/t2501-cwd-empty.sh | 4 ++--\n>  2 files changed, 5 insertions(+), 2 deletions(-)\n> \n> diff --git a/sequencer.c b/sequencer.c\n> index ea96837cde3..b71f7b8a0a6 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -4228,6 +4228,9 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n>  \n>  \tcmd.git_cmd = 1;\n>  \n> +\tif (startup_info->original_cwd &&\n> +\t    !is_absolute_path(startup_info->original_cwd))\n> +\t\tcmd.dir = startup_info->original_cwd;\n\nI was initially confused by the \"!is_absolute_path()\" because\nit seemed to me like it would be natural to store an absolute\npath there, but I see this comment in patch 2:\n\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n\nSo it seems that we won't store it as an absolute path. Is\nthere any value in this condition, then?\n\nThis assignment of cmd.dir to the relative path has a lot\nof baked-in knowledge of this variable _and_ the current\nstate (Git chdir()'d to the root of the worktree). If the\npath is always relative, then it should be a BUG() if we\nsee an absolute path. Also, it seems like we would want\ncmd.dir to be a concatenation of the worktree root and the\noriginal_cwd.\n\nOr perhaps I'm being overly cautious and this could all be\nresolved with a comment about the expected state of the\nworking directory and original_cwd. The tests will catch if\nany of those expectations change.\n\nThanks,\n-Stolee\n"},{"id":"442546","messageId":"068b7faf-2ade-14a7-fee3-83fec26ae856@gmail.com","threadId":"56952","inReplyTo":"CABPp-BHStJ2x53cffJPb7miaxL2cKr0usgK13HxNDyYMQv11_g@mail.gmail.com","subject":"Re: [PATCH v2 2/9] setup: introduce startup_info->original_cwd","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T17:43:17Z","receivedAt":"2021-11-29T22:47:18Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"\n\nOn 11/29/2021 12:18 PM, Elijah Newren wrote:\n> On Mon, Nov 29, 2021 at 6:05 AM Derrick Stolee <stolee@gmail.com> wrote:\n>>\n>> On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n>>> From: Elijah Newren <newren@gmail.com>\n>>\n>>> @@ -866,6 +866,8 @@ int cmd_main(int argc, const char **argv)\n>>>\n>>>       trace_command_performance(argv);\n>>>\n>>> +     startup_info->original_cwd = xgetcwd();\n>>> +\n>>\n>> I see this initial assignment in cmd_main()...\n> \n> It looks like you accidentally responded to v2 when there's a v3\n> (something I occasionally do too).  v3 changes this to put it in\n> common-main instead of here, as suggested by Ævar, but to answer the\n> question...\n\nYes, sorry about that. My inbox was delayed in showing me that a v3\nexisted until I was halfway through reviewing v2. (It then only\nshowed me half of the patches from v3, so something was causing\na delay.)\n\n>>> +static void setup_original_cwd(void)\n>>> +{\n>>> +     struct strbuf tmp = STRBUF_INIT;\n>>> +     const char *worktree = NULL;\n>>> +     int offset = -1;\n>>> +\n>>> +     /*\n>>> +      * startup_info->original_cwd wass set early on in cmd_main(), unless\n>>> +      * we're an auxiliary tool like git-remote-http or test-tool.\n>>> +      */\n>>> +     if (!startup_info->original_cwd)\n>>> +             return;\n>>\n>> ...which is assumed to be run before this method was called...\n>>\n>>> @@ -1330,6 +1378,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>>>               setenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n>>>       }\n>>>\n>>> +     setup_original_cwd();\n>>\n>> ...here in setup_git_directory_gently().\n>>\n>> Why do we need that assignment in cmd_main()? Could we instead\n>> let setup_original_cwd() do the initial assignment? Or is it\n>> possible that a chdir has happened already before this point?\n> \n> In v1, I made that mistake.  Then I realized that when users pass the\n> -C option to git, there is a chdir() call immediately upon parsing of\n> the -C option.  So I had to move the strbuf_getcwd() call earlier.\n\nOk. I wonder if we could setup_original_cwd() earlier than that\nparsing, but I'm sure you've already explored that option.\n\nThanks,\n-Stolee\n"},{"id":"442552","messageId":"CABPp-BGSNh0Ai6nT0B+kW=37w=ho6SRNoTEGFXE9uaqxmfwmAQ@mail.gmail.com","threadId":"56952","inReplyTo":"f65473df-4cb0-afdb-70e3-ee7685e6d817@gmail.com","subject":"Re: [PATCH v2 4/9] unpack-trees: add special cwd handling","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-29T17:33:33Z","receivedAt":"2021-11-29T22:56:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 6:14 AM Derrick Stolee <stolee@gmail.com> wrote:\n>\n> On 11/25/2021 3:39 AM, Elijah Newren via GitGitGadget wrote:\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > When running commands such as `git reset --hard` from a subdirectory, if\n> > that subdirectory is in the way of adding needed files, bail with an\n> > error message.\n> ...\n> > -test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n> > +test_expect_success 'reset --hard fails if cwd needs to be removed' '\n> >       git checkout foo/bar/baz &&\n> >       test_when_finished \"git clean -fdx\" &&\n>\n> This test doesn't include any condition on the final state of\n> the worktree. I could imagine that an implementation could be\n> writing to the worktree incrementally and then bail halfway\n> through. A clean \"git status\" would give us some confidence that\n> the worktree and index are in a good state after the command\n> fails.\n\nMakes sense; I can add a\n   git diff --exit-code HEAD &&\nto the test.\n\n> > +     if (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n> > +             /* Avoid nuking startup_info->original_cwd... */\n> > +             if (startup_info->original_cwd &&\n> > +                 !strcmp(startup_info->original_cwd, ce->name))\n> > +                     return add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n> > +                                              ce->name);\n> > +             /* ...but nuke anything else. */\n> > +             return 0;\n> > +     }\n> > +\n>\n> This implementation appears to do the right thing in the case\n> I'm describing: we check which entries would need to change and\n> we reject them before doing any real work.\n\nThanks for taking a look.\n"},{"id":"442554","messageId":"CABPp-BFGkQHBF+zd+WRaSxN_BWm62x9Cq89b0AXxkL+8pSdRPg@mail.gmail.com","threadId":"56952","inReplyTo":"211129.868rx6g0e0.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v3 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-29T21:58:08Z","receivedAt":"2021-11-29T22:58:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 12:41 PM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n>\n> On Sat, Nov 27 2021, Elijah Newren wrote:\n>\n> > On Sat, Nov 27, 2021 at 6:00 AM Ævar Arnfjörð Bjarmason\n> > <avarab@gmail.com> wrote:\n> >>\n> >> On Fri, Nov 26 2021, Elijah Newren via GitGitGadget wrote:\n> >>\n> >> [Just some more \"I haven't really looked at this in all that much\n> >> detail\" commentary, so maybe it's stupid, sorry]\n> >>\n> >> > From: Elijah Newren <newren@gmail.com>\n> >> >\n> >> > Removing the current working directory causes all subsequent git\n> >> > commands run from that directory to get confused and fail with a message\n> >> > about being unable to read the current working directory:\n> >> >\n> >> >     $ git status\n> >> >     fatal: Unable to read current working directory: No such file or directory\n> >> >\n> >> > Non-git commands likely have similar warnings or even errors, e.g.\n> >> >\n> >> >     $ bash -c 'echo hello'\n> >> >     shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n> >> >     hello\n> >>\n> >> Is that really realistic?\n> >\n> > I have 321 shell scripts (or symlinks thereto) in /usr/bin/ on my\n> > Fedora laptop, and 951 shell scripts in /usr/bin/ on my Ubuntu\n> > workstation at $DAYJOB.  That's not even considering stuff in other\n> > directories.  Note that I didn't place any of those scripts in\n> > /usr/bin/; they came from the distribution and/or corporate packages\n> > from others.  Any invocation of bash will see the above \"error\"\n> > message.  Granted, bash calls it an 'error' but continues anyway, but\n> > I thought it showed that there were clearly programs besides git where\n> > users could run into problems.\n> >\n> >> Any \"normal\" command would use \"pwd\" or look\n> >> at $PWD, both of which \"work\", this error is only because we're starting\n> >> a new shell.\n> >\n> > <grin>\n> >\n> > Yeah, good point.  Who would use an unusual program like git?  Or a\n> > shell script?  Or a java program?  Or emacs?  Or other programs like\n> > them?  Besides, git, bash, java, and emacs are all relatively young\n> > commands with small development teams.  Maybe we should just rely on\n> > users only using commands that get the pwd/$PWD detail right; perhaps\n> > limiting to commands that are more mature and have bigger development\n> > teams than those four.\n> >\n> > Silly users.\n> >\n> > ;-)\n> >\n> > Sorry, couldn't resist a little friendly teasing.\n> >\n> >\n> > However, for \"pwd\"...by \"work\", do you mean \"doesn't necessarily\n> > work\"?  On my machine:\n> >\n> > $ mkdir gone\n> > $ cd gone\n> > $ rmdir ../gone\n> > $ pwd -P\n> > pwd: error retrieving current directory: getcwd: cannot access parent\n> > directories: No such file or directory\n> > $ pwd\n> > pwd: error retrieving current directory: getcwd: cannot access parent\n> > directories: No such file or directory\n> > $ echo $PWD\n> > /home/newren/floss/git/gone\n> >\n> > If I do not run `pwd -P` first then a plain `pwd` works.  But your\n> > advice to use `pwd` seems misguided for programs, since they'll hit\n> > this problem if users have run a previous `pwd -P`.  (The $PWD trick\n> > would have worked, as shown above)\n> >\n> >> I wonder if it was just because you ran into our bin-wrappers edge case,\n> >> but that should be really obscure for any real users.\n>\n> For some reason I was under the misimpression that the \"#!/bin/bash\"\n> part of the bin-wrappers and other scripts somehow immunized them from\n> the $PWD/\"pwd\" reset, and it was only the programs they invoked (like\n> git in the bin-wrappers) that didn't get the values passed along.\n>\n> But that's clearly incorrect as you demonstrate above, so the only thing\n> that'll work OK (seemingly) is running \"pwd\" (but not \"pwd -P\"), or\n> looking at $PWD in your terminal shell itself.\n\nAnd further, even running a plain \"pwd\" seems to not work either if\nsomething else has already run \"pwd -P\" already, as I pointed out\nabove.  I have no clue how one 'pwd -P' call can prevent future plain\n'pwd' calls from working, but that's the behavior I observed.\n\n> Invoking non-POSIX shell programs \"works\" in that they can use the same\n> trick, after the dance of \"mkdir x && cd x && rm -rf ..x\" e.g. Perl\n> says:\n>\n>     $ perl -MCwd=getcwd -wE 'say $ENV{PWD}; say getcwd'\n>     /home/avar/g/git/x\n>     Use of uninitialized value in say at -e line 1\n>\n> This \"pwd -P\" behavior isn't just something weird in your shell & mine,\n> it semse to be mandated by POSIX:\n> https://pubs.opengroup.org/onlinepubs/007904875/utilities/pwd.html\n>\n> All of which is to say I'm much more sympathetic to this approach\n> now. I.e. it seemed like purely a way to work around fixable breakages\n> in our own and other programs. Well, I guess \"don't use shellscript if\n> you care about this edge case\" is a \"fix\", but not a trivial one.\n>\n> I do still think a better starting point would be fixing the setup.c\n> dying in our own code, and see where that leaves us, but up to you\n> etc. I'm not going to work on it any time soon.\n>\n> > Yes, I agree our bin-wrappers is a really obscure edge case not worth\n> > considering.  I don't see how you jump from there to painting all\n> > shell scripts combined with the same brush, though.\n>\n> *nod*\n>\n> >> > This confuses end users, particularly since the command they get the\n> >> > error from is not the one that caused the problem; the problem came from\n> >> > the side-effect of some previous command.\n> >> >\n> >> > We would like to avoid removing the current working directory of our\n> >> > parent process; towards this end, introduce a new variable,\n> >> > startup_info->original_cwd, that tracks the current working directory\n> >> > that we inherited from our parent process.  For convenience of later\n> >> > comparisons, we prefer that this new variable store a path relative to\n> >> > the toplevel working directory (thus much like 'prefix'), except without\n> >> > the trailing slash.\n> >>\n> >> I'm still not clear at all on why we need a \"original_cwd\" at all then\n> >> as opposed to just using \"prefix\" (or adding \"the_prefix\" if passing it\n> >> down is painful). I.e. we discover our relative path, we resolve the\n> >> relative path to the root, can't we use that as the \"don't remove our\n> >> CWD\" guard?\n> >>\n> >> Does our prefix change at some point, then maybe \"orig_prefix\" would\n> >> make more sense?\n> >\n> > No; see the code comment in the same patch:\n> >\n> > +       /*\n> > +        * startup_info->original_cwd points to the current working\n> > +        * directory we inherited from our parent process, which is a\n> > +        * directory we want to avoid removing.\n> > +        *\n> > +        * For convience, we would like to have the path relative to the\n> > +        * worktree instead of an absolute path.\n> > +        *\n> > +        * Yes, startup_info->original_cwd is usually the same as 'prefix',\n> > +        * but differs in two ways:\n> > +        *   - prefix has a trailing '/'\n> > +        *   - if the user passes '-C' to git, that modifies the prefix but\n> > +        *     not startup_info->original_cwd.\n> > +        */\n> >\n> > It's never equal to prefix, even though it's usually semantically\n> > referring to the same directory.  However, even if it weren't for the\n> > trailing slash issue, the -C case means it is not appropriate to think\n> > of it as \"orig_prefix\" either.\n>\n> Ah, with -C of e.g. t/helper we'll first chdir(t/helper), and then run\n> the usual setup.c dance to find that we need to chdir() again to the\n> (equivalent of) \"../../\". But our prefix stays at \"t/helper\".\n>\n> I'm a bit confused about the trailing slash case, isn't the prefix\n> always going to point to a directory? Why would t/helper v.s. t/helper/\n> matter?\n\nBecause when you compare two paths, you either need to canonicalize\nthem in some fashion or do a bunch of tricky work to see if they are\n\"equal\".\n\n> I think it won't matter for rmdir(2) et al, but maybe I'm wrong.\n\nCorrect, rmdir won't care, but strcmp() certainly does.  Since we call\nstrcmp() before deciding whether to remove, and we do that based on\npaths built up from prefix + tree traversal + entry name (which won't\nhave the trailing '/') we want original_cmp to be canonicalized the\nsame way (i.e. without the trailing '/').\n\n(In contrast, prefix is used as a starting point to build up other\npaths, so having a trailing slash was more convenient for it.)\n\n> What got me confused about the \"prefix\" v.s. \"original_cwd\" is that I\n> was assuming they'd be the same. The commentary on setup_git_directory()\n> says as much, i.e.:\n>\n>     Returns the \"prefix\", a path to the current working directory\n>     relative to the work tree root, or NULL,\n>\n> But of course we know that's a white lie, it's not the $PWD/getcwd().\n\nI don't think it is a white lie.  This code runs after the chdir()\nfrom the parsing of the global -C option, and before the chdir() to\nthe worktree root (which occurs later in setup.c).  So the statement\nis true, you just have to understand what \"current\" means in \"the\ncurrent working directory\".  In this context, \"current\" means *as of\nthe time the setup.c code where this comment is written starts\nrunning*, which is a pretty logical thing for current to mean.  If you\ntry reading this code unaware of the other chdir() calls we make,\nthough, it certainly has the potential to trip you up.\n\n> So you're only trying to save the user in cases of e.g. (in t/helper):\n>\n>     git rm ../helper\n>\n> If they actually run it while in t/helper, but would like to explicitly\n> omit the case of (at the top-level):\n>\n>     git -C t/helper rm ../helper\n>\n> That's fair enough I guess. I'd just assumed those cases would be\n> treated the same way.\n\nI don't see why you'd assume that.  The whole point was preserving the\ncwd of our parent process so that any subsequent commands that happen\nto run in that parent process don't get confused by having their cwd\nno longer exist.  In the former command you gave, the cwd of the\nparent process was $WORKTREE/t/helper; in the latter, it was\n$WORKTREE.  So, they should preserve different things.\n\n> Even in that case, I can't think of a case where this \"original_cwd\"\n> wouldn't be made redundant by some boolean flag to accompany \"prefix\" to\n> indicate that we've chdir'd out of the prefix (or not).\n\nI can't think of a case where a boolean would provide any useful\ninformation.  Don't we always chdir out of the prefix whenever prefix\n!= NULL?\n\nMaybe I'm missing something, so let me give you a simple case where\nyou can explain how this boolean could help.  Let's say you started at\nthe worktree root and then ran these two commands:\n\n   $ cd subdir/\n   $ git -C subsubdir rm lastfile\n\nAfter finishing the setup phase of the git command, the state should\nbe the following:\n   prefix = subdir/subsubdir/\n   original_cwd = subdir\n   getcwd = subdir/../  (because setup.c chdirs to the worktree root;\nthe actual answer would be a more canonicalized path, though)\n\nIn this above case, how can we infer the value of original_cwd from\nother data if we don't record it?\n"},{"id":"442556","messageId":"d0f7eb3e-fb8d-a1d5-b582-c2eb23ebb1f5@gmail.com","threadId":"56952","inReplyTo":"CABPp-BFr-U1kaqaj9HR+A=tRt0F_btcbDp89xeoZRdXewM4a2w@mail.gmail.com","subject":"Re: [PATCH v3 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-29T19:42:48Z","receivedAt":"2021-11-29T23:03:19Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 11/29/2021 2:22 PM, Elijah Newren wrote:\n> On Mon, Nov 29, 2021 at 9:50 AM Derrick Stolee <stolee@gmail.com> wrote:\n>>\n>> On 11/26/2021 5:40 PM, Elijah Newren via GitGitGadget wrote:\n>>>       cmd.git_cmd = 1;\n>>>\n>>> +     if (startup_info->original_cwd &&\n>>> +         !is_absolute_path(startup_info->original_cwd))\n>>> +             cmd.dir = startup_info->original_cwd;\n>>\n>> I was initially confused by the \"!is_absolute_path()\" because\n>> it seemed to me like it would be natural to store an absolute\n>> path there, but I see this comment in patch 2:\n>>\n>> +        * For convience, we would like to have the path relative to the\n>> +        * worktree instead of an absolute path.\n>>\n>> So it seems that we won't store it as an absolute path. Is\n>> there any value in this condition, then?\n> \n> Good catch.  This is leftover from an earlier round; I'll remove it.\n> From stash too.\n> \n>> This assignment of cmd.dir to the relative path has a lot\n>> of baked-in knowledge of this variable _and_ the current\n>> state (Git chdir()'d to the root of the worktree). If the\n>> path is always relative, then it should be a BUG() if we\n>> see an absolute path. Also, it seems like we would want\n>> cmd.dir to be a concatenation of the worktree root and the\n>> original_cwd.\n>>\n>> Or perhaps I'm being overly cautious and this could all be\n>> resolved with a comment about the expected state of the\n>> working directory and original_cwd. The tests will catch if\n>> any of those expectations change.\n> \n> Yeah, with the extra condition in there I should have added a good\n> explanation about why I had it in there.  But I think a simple\n> \n> if (startup_info->original_cwd)\n>     cmd.dir = startup_info->original_cwd;\n> \n> should be good enough?  Or do you still want a comment for that?\n \nThis is the code I would expect to see. No comment needed.\n\nThanks,\n-Stolee\n"},{"id":"442561","messageId":"1990e36bb4121c6b79cee4b646f6adc7ad3f3424.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 06/11] clean: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:09Z","receivedAt":"2021-11-29T23:09:55Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 44 +++++++++++++++++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  5 +++--\n 2 files changed, 38 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..3ff02bbbffe 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n {\n \tDIR *dir;\n \tstruct strbuf quoted = STRBUF_INIT;\n+\tstruct strbuf realpath = STRBUF_INIT;\n+\tstruct strbuf real_ocwd = STRBUF_INIT;\n \tstruct dirent *e;\n \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n \tstruct string_list dels = STRING_LIST_INIT_DUP;\n@@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t/*\n+\t\t * Normalize path components in path->buf, e.g. change '\\' to\n+\t\t * '/' on Windows.\n+\t\t */\n+\t\tstrbuf_realpath(&realpath, path->buf, 1);\n+\n+\t\t/*\n+\t\t * path and realpath are absolute; for comparison, we would\n+\t\t * like to transform startup_info->original_cwd to an absolute\n+\t\t * path too.\n+\t\t */\n+\t\t if (startup_info->original_cwd)\n+\t\t\t strbuf_realpath(&real_ocwd,\n+\t\t\t\t\t startup_info->original_cwd, 1);\n+\n+\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n \t}\n out:\n+\tstrbuf_release(&realpath);\n+\tstrbuf_release(&real_ocwd);\n \tstrbuf_release(&quoted);\n \tstring_list_clear(&dels, 0);\n \treturn ret;\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 50619de10aa..6ab82e05d98 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -230,8 +230,9 @@ test_incidental_untracked_dir_removal () {\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n-\t\tgit -C .. clean -fd -e warnings . >warnings\n+\ttest_incidental_untracked_dir_removal success \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings &&\n+\tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442562","messageId":"2e2ea02f97b1093fcf89cb05e54e21fdb45273d0.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 04/11] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:07Z","receivedAt":"2021-11-29T23:09:58Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the_original_cwd\nbeing in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\nthere is no untracked checking to be done, so we simply add a special\ncase near the top of verify_absent_1.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 13 +++++++++++--\n 2 files changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 55081f5c980..10e4654042c 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -121,7 +121,7 @@ test_expect_success 'reset --hard does not clean cwd incidentally' '\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git reset --hard fd_conflict\n+\ttest_required_dir_removal success git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 6bc16f3a714..5852807d2fb 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking startup_info->original_cwd... */\n+\t\tif (startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"442563","messageId":"d5750fcb6d561f0190ae69ac0065a6971c437c81.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:13Z","receivedAt":"2021-11-29T23:10:02Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nremove_dir_recurse(), and its non-static wrapper called\nremove_dir_recursively(), both take flags for modifying its behavior.\nAs with the previous commits, we would generally like to protect\nthe original_cwd, but we want to forced user commands (e.g. 'git rm -rf\n...') or other special cases to remove it.  Add a flag for this purpose.\nAfter reading through every caller of remove_dir_recursively() in the\ncurrent codebase, there was only one that should be adjusted and that\none only in a very unusual circumstance.  Add a pair of new testcases to\nhighlight that very specific case involving submodules && --git-dir &&\n--work-tree.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/rm.c         |  3 ++-\n dir.c                | 12 +++++++++---\n dir.h                |  3 +++\n t/t2501-cwd-empty.sh |  5 -----\n 4 files changed, 14 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/rm.c b/builtin/rm.c\nindex 3d0967cdc11..b4132e5d8ee 100644\n--- a/builtin/rm.c\n+++ b/builtin/rm.c\n@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)\n \tif (!index_only) {\n \t\tint removed = 0, gitmodules_modified = 0;\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tint flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;\n \t\tfor (i = 0; i < list.nr; i++) {\n \t\t\tconst char *path = list.entry[i].name;\n \t\t\tif (list.entry[i].is_submodule) {\n \t\t\t\tstrbuf_reset(&buf);\n \t\t\t\tstrbuf_addstr(&buf, path);\n-\t\t\t\tif (remove_dir_recursively(&buf, 0))\n+\t\t\t\tif (remove_dir_recursively(&buf, flag))\n \t\t\t\t\tdie(_(\"could not remove '%s'\"), path);\n \n \t\t\t\tremoved = 1;\ndiff --git a/dir.c b/dir.c\nindex 97d6b71c872..52064345a6b 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tint ret = 0, original_len = path->len, len, kept_down = 0;\n \tint only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);\n \tint keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);\n+\tint purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);\n \tstruct object_id submodule_head;\n \n \tif ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&\n@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (!purge_original_cwd &&\n+\t\t    startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\ndiff --git a/dir.h b/dir.h\nindex d6a5d03bec2..8e02dfb505d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n /* Remove the contents of path, but leave path itself. */\n #define REMOVE_DIR_KEEP_TOPLEVEL 04\n \n+/* Remove the_original_cwd too */\n+#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08\n+\n /*\n  * Remove path and its contents, recursively. flags is a combination\n  * of the above REMOVE_DIR_* constants. Return 0 on success.\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 30b8ffaa11b..6d8f68c08dd 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -291,11 +291,6 @@ test_submodule_removal () {\n \ttest_status=\n \ttest \"$path_status\" = dir && test_status=test_must_fail\n \n-\t# Actually, while path_status == dir && test_status=test_must_fail\n-\t# reflect our desired behavior, current behavior is:\n-\tpath_status=missing\n-\ttest_status=\n-\n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442564","messageId":"f444a541da44c9fb16d9d3dea37f85cfc0c2edcf.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 05/11] symlinks: do not include startup_info->original_cwd in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:08Z","receivedAt":"2021-11-29T23:10:09Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude startup_info->original_cwd and leave it around.  This\navoids the user getting confused by subsequent git commands (and non-git\ncommands) that would otherwise report confusing messages about being\nunable to read the current working directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           |  8 +++++++-\n t/t2501-cwd-empty.sh | 10 +++++-----\n 2 files changed, 12 insertions(+), 6 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..c667baa949b 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif ((startup_info->original_cwd &&\n+\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n+\t\t    rmdir(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(name, startup_info->original_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 10e4654042c..50619de10aa 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -109,7 +109,7 @@ test_required_dir_removal () {\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git checkout init\n+\ttest_incidental_dir_removal success git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n@@ -117,7 +117,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git reset --hard init\n+\ttest_incidental_dir_removal success git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n@@ -125,7 +125,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git merge reverted\n+\ttest_incidental_dir_removal success git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -158,7 +158,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git cherry-pick reverted\n+\ttest_incidental_dir_removal success git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n@@ -174,7 +174,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git revert HEAD\n+\ttest_incidental_dir_removal success git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-- \ngitgitgadget\n\n"},{"id":"442565","messageId":"a2be40a22d1cab518fd006c2646ba8ada29b8716.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 08/11] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:11Z","receivedAt":"2021-11-29T23:10:13Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince stash spawns a `clean` subprocess, make sure we run that from the\nstartup_info->original_cwd directory, so that the `clean` processs knows\nto protect that directory.  Also, since the `clean` command might no\nlonger run from the toplevel, pass the ':/' magic pathspec to ensure we\nstill clean from the toplevel.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 4 +++-\n t/t2501-cwd-empty.sh | 2 +-\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..de0e432a4ff 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (startup_info->original_cwd)\n+\t\t\t\tcp.dir = startup_info->original_cwd;\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 4f7dba9ab3d..e2586a63eca 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n+\ttest_incidental_untracked_dir_removal success \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n\n"},{"id":"442566","messageId":"41a82eff41e0500a6fca9672f8acec1794cdaf55.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 03/11] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:06Z","receivedAt":"2021-11-29T23:10:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory we inherited\nfrom our parent process.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 20 +++++++-------------\n unpack-trees.c       | 17 +++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 21 insertions(+), 17 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex fd83fe921d5..55081f5c980 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -113,7 +113,7 @@ test_expect_success 'checkout does not clean cwd incidentally' '\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git checkout fd_conflict\n+\ttest_required_dir_removal success git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n@@ -144,23 +144,17 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We would rather this failed, but we test for existing\n-\t\t# rather than desired behavior\n-\t\tgit merge fd_conflict 2>../error\n+\t\ttest_must_fail git merge fd_conflict 2>../error\n \t) &&\n \n-\t## Here is the behavior we would rather have:\n-\t#test_path_is_dir dirORfile &&\n-\t#grep \"Refusing to remove the current working directory\" error\n-\t## But instead we test for existing behavior\n-\ttest_path_is_file dirORfile &&\n-\ttest_must_be_empty error\n+\ttest_path_is_dir dirORfile &&\n+\tgrep \"Refusing to remove the current working directory\" error\n '\n \n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git merge fd_conflict\n+\ttest_required_dir_removal success git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n@@ -168,7 +162,7 @@ test_expect_success 'cherry-pick does not clean cwd incidentally' '\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+\ttest_required_dir_removal success git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n@@ -184,7 +178,7 @@ test_expect_success 'revert does not clean cwd incidentally' '\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git revert undo_fd_conflict\n+\ttest_required_dir_removal success git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..6bc16f3a714 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose startup_info->original_cwd */\n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"442567","messageId":"1035ee7f9ce174279b8065edf948b76b82281268.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:10Z","receivedAt":"2021-11-29T23:10:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince rebase spawns a `checkout` subprocess, make sure we run that from\nthe startup_info->original_cwd directory, so that the checkout process\nknows to protect that directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n sequencer.c          | 2 ++\n t/t2501-cwd-empty.sh | 4 ++--\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex ea96837cde3..83f257e7fa4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n \n \tcmd.git_cmd = 1;\n \n+\tif (startup_info->original_cwd)\n+\t\tcmd.dir = startup_info->original_cwd;\n \tstrvec_push(&cmd.args, \"checkout\");\n \tstrvec_push(&cmd.args, commit);\n \tstrvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 6ab82e05d98..4f7dba9ab3d 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rebase reverted\n+\ttest_incidental_dir_removal success git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git rebase fd_conflict\n+\ttest_required_dir_removal success git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442568","messageId":"834031be9e04f590d219d33250699200db3c85c4.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 09/11] dir: avoid incidentally removing the original_cwd in remove_path()","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:12Z","receivedAt":"2021-11-29T23:10:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nModern git often tries to avoid leaving empty directories around when\nremoving files.  Originally, it did not bother.  This behavior started\nwith commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\ndirectories, 2005-11-19), stating the reason simply as:\n\n    When the last file in a directory is removed as the result of a\n    merge, try to rmdir the now-empty directory.\n\nThis was reimplemented in C and renamed to remove_path() in commit\ne1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\ninternal to merge-recursive.\n\nThis trend towards removing leading empty directories continued with\ncommit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\nstated the reasoning as:\n\n    The other question is what to do with leading directories. The old\n    \"git rm\" script didn't do anything, which is somewhat inconsistent.\n    This one will actually clean up directories that have become empty\n    as a result of removing the last file, but maybe we want to have a\n    flag to decide the behaviour?\n\nremove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\nfunction to remove as much as possible of a path, 2008-09-27), because\nit was noted that we had two separate implementations of the same idea\nAND both were buggy.  It described the purpose of the function as\n\n    a function to remove as much as possible of a path\n\nWhy remove as much as possible?  Well, at the time we probably would\nhave said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nBut I don't believe those reasons hold when the empty directory happens\nto be the current working directory we inherited from our parent\nprocess.  Leaving the parent process in a deleted directory can cause\nuser confusion when subsequent processes fail: any git command, for\nexample, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nOther commands may similarly get confused.  Modify remove_path() so that\nthe empty leading directories it also deletes does not include the\ncurrent working directory we inherited from our parent process.  I have\nlooked through every caller of remove_path() in the current codebase to\nmake sure that all should take this change.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                |  3 +++\n dir.h                |  6 +++++-\n t/t2501-cwd-empty.sh | 12 ++++--------\n 3 files changed, 12 insertions(+), 9 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..97d6b71c872 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3327,6 +3327,9 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/dir.h b/dir.h\nindex 83f46c0fb4c..d6a5d03bec2 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n  */\n int remove_dir_recursively(struct strbuf *path, int flag);\n \n-/* tries to remove the path with empty directories along it, ignores ENOENT */\n+/*\n+ * Tries to remove the path, along with leading empty directories so long as\n+ * those empty directories are not startup_info->original_cwd.  Ignores\n+ * ENOENT.\n+ */\n int remove_path(const char *path);\n \n int fspathcmp(const char *a, const char *b);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex e2586a63eca..30b8ffaa11b 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -182,12 +182,12 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rm bar/baz.t\n+\ttest_incidental_dir_removal success git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal failure git apply ../patch\n+\ttest_incidental_dir_removal success git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n@@ -271,12 +271,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n \t) &&\n \n \ttest_path_is_missing a/b/c/tracked &&\n-\t## We would prefer if a/b was still present, though empty, since it\n-\t## was the current working directory\n-\t#test_path_is_dir a/b\n-\t## But the current behavior is that it not only deletes the directory\n-\t## a/b as requested, but also goes and deletes a\n-\ttest_path_is_missing a\n+\ttest_path_is_missing a/b/c &&\n+\ttest_path_is_dir a/b\n '\n \n test_expect_success 'git version continues working from a deleted dir' '\n-- \ngitgitgadget\n\n"},{"id":"442569","messageId":"a45b3f088025795d11a78055f90b8632d435d74f.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:04Z","receivedAt":"2021-11-29T23:10:20Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove directories left empty as a \"convenience\"\nafter removing files within them.  That is normally fine, but removing\nthe current working directory can be rather inconvenient since it can\ncause confusion for the user when they run subsequent commands.  For\nexample, after one git process has removed the current working\ndirectory, git status/log/diff will all abort with the message:\n\n    fatal: Unable to read current working directory: No such file or directory\n\nWe also have code paths that, when a file needs to be placed where a\ndirectory is (due to e.g. checkout, merge, reset, whatever), will check\nif this is okay and error out if not.  These rules include:\n  * all tracked files under that directory are intended to be removed by\n    the operation\n  * none of the tracked files under that directory have uncommitted\n    modification\n  * there are no untracked files under that directory\nHowever, if we end up remove the current working directory, we can cause\nuser confusion when they run subsequent commands, so we would prefer if\nthere was a fourth rule added to this list: avoid removing the current\nworking directory.\n\nSince there are several code paths that can result in the current\nworking directory being removed, add several tests of various different\ncodepaths.  To make it clearer what the difference between the current\nbehavior and the behavior at the end of the series, code both of them\ninto the tests and have the appropriate behavior be selected by a flag.\nSubsequent commits will toggle the flag from current to desired\nbehavior.\n\nAlso add a few tests suggested during the review of earlier rounds of\nthis patch series.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 342 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..fd83fe921d5\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,342 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tmkdir dirORfile &&\n+\ttest_commit dirORfile/foo &&\n+\n+\tgit rm -r dirORfile &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile &&\n+\n+\tgit switch -c df_conflict HEAD~1 &&\n+\ttest_commit random_file &&\n+\n+\tgit switch -c undo_fd_conflict fd_conflict &&\n+\tgit revert HEAD\n+'\n+\n+test_incidental_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" == \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" == \"success\"\n+\tthen\n+\t\ttest_path_is_dir foo\n+\telse\n+\t\ttest_path_is_missing foo\n+\tfi\n+}\n+\n+test_required_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\tgit checkout df_conflict^{commit} &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We'd like for the command to fail (much as it would if there\n+\t\t# was an untracked file there), and for the index and worktree\n+\t\t# to be left clean with pwd and git status working afterwards.\n+\t\t# But test for existing rather than desired behavior.\n+\t\tif test \"$works\" == \"success\"\n+\t\tthen\n+\t\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t\tgit diff --exit-code HEAD &&\n+\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t\"$@\" &&\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\n+\t# Although we want dirORfile to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" == \"success\"\n+\tthen\n+\t\ttest_path_is_dir dirORfile\n+\telse\n+\t\ttest_path_is_file dirORfile\n+\tfi\n+}\n+\n+test_expect_success 'checkout does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git checkout init\n+'\n+\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git checkout fd_conflict\n+'\n+\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git reset --hard init\n+'\n+\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git reset --hard fd_conflict\n+'\n+\n+test_expect_success 'merge does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git merge reverted\n+'\n+\n+# This file uses some simple merges where\n+#   Base: 'dirORfile/' exists\n+#   Side1: random other file changed\n+#   Side2: 'dirORfile/' removed, 'dirORfile' added\n+# this should resolve cleanly, but merge-recursive throws merge conflicts\n+# because it's dumb.  Add a special test for checking merge-recursive (and\n+# merge-ort), then after this just hard require ort for all remaining tests.\n+#\n+test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We would rather this failed, but we test for existing\n+\t\t# rather than desired behavior\n+\t\tgit merge fd_conflict 2>../error\n+\t) &&\n+\n+\t## Here is the behavior we would rather have:\n+\t#test_path_is_dir dirORfile &&\n+\t#grep \"Refusing to remove the current working directory\" error\n+\t## But instead we test for existing behavior\n+\ttest_path_is_file dirORfile &&\n+\ttest_must_be_empty error\n+'\n+\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n+test_expect_success 'merge fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git merge fd_conflict\n+'\n+\n+test_expect_success 'cherry-pick does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git cherry-pick reverted\n+'\n+\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+'\n+\n+test_expect_success 'rebase does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rebase reverted\n+'\n+\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git rebase fd_conflict\n+'\n+\n+test_expect_success 'revert does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git revert HEAD\n+'\n+\n+test_expect_success 'revert fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git revert undo_fd_conflict\n+'\n+\n+test_expect_success 'rm does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rm bar/baz.t\n+'\n+\n+test_expect_success 'apply does not remove cwd incidentally' '\n+\tgit diff HEAD HEAD~1 >patch &&\n+\ttest_incidental_dir_removal failure git apply ../patch\n+'\n+\n+test_incidental_untracked_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\tmkdir -p untracked &&\n+\tmkdir empty\n+\t>untracked/random &&\n+\n+\t(\n+\t\tcd untracked &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" == \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" == \"success\"\n+\tthen\n+\t\ttest_path_is_dir untracked\n+\telse\n+\t\ttest_path_is_missing untracked\n+\tfi\n+}\n+\n+test_expect_success 'clean does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings\n+'\n+\n+test_expect_success 'stash does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit stash --include-untracked\n+'\n+\n+test_expect_success '`rm -rf dir` only removes a subset of dir' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/untracked &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_dir a/b &&\n+\ttest_path_is_missing a/b/c/tracked &&\n+\ttest_path_is_file a/b/c/untracked\n+'\n+\n+test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_missing a/b/c/tracked &&\n+\t## We would prefer if a/b was still present, though empty, since it\n+\t## was the current working directory\n+\t#test_path_is_dir a/b\n+\t## But the current behavior is that it not only deletes the directory\n+\t## a/b as requested, but also goes and deletes a\n+\ttest_path_is_missing a\n+'\n+\n+test_expect_success 'git version continues working from a deleted dir' '\n+\tmkdir tmp &&\n+\t(\n+\t\tcd tmp &&\n+\t\trm -rf ../tmp &&\n+\t\tgit version\n+\t)\n+'\n+\n+test_submodule_removal () {\n+\tpath_status=$1 &&\n+\tshift &&\n+\n+\ttest_status=\n+\ttest \"$path_status\" = dir && test_status=test_must_fail\n+\n+\t# Actually, while path_status == dir && test_status=test_must_fail\n+\t# reflect our desired behavior, current behavior is:\n+\tpath_status=missing\n+\ttest_status=\n+\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\t$test_status \"$@\"\n+\t) &&\n+\n+\ttest_path_is_${path_status} my_submodule\n+}\n+\n+test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n+'\n+\n+test_expect_success 'rm -r leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n+'\n+\n+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n+\ttest_submodule_removal missing \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"442570","messageId":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"[PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:03Z","receivedAt":"2021-11-29T23:10:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). Many commands either\nsilently avoid removing directories that are not empty (i.e. those that have\nuntracked or modified files in them)[1], or show an error and abort,\ndepending on which is more appropriate for the command in question. With\nthis series, we augment the reasons to avoid removing directories to include\nnot just has-untracked-or-modified-files, but also to avoid removing the\noriginal_cwd as well.\n\nPeff and Junio provided some good pros/cons, if it helps:\n\n * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\nhttps://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\nChanges since v3:\n\n * fixed one codepath from v2 so that the series really is only about the\n   working tree\n * used test-tool getcwd instead of pwd -P as suggested by Ævar for some\n   less common platforms\n * fixed bashism\n * check for clean index/worktree after verifying that expected-to-abort\n   codepaths do abort, to make it clearer that we expect an early abort\n * remove a leftover (and confusing) is_absolute_dir() check in sequencer\n   and stash from an earlier round of the series\n\nChanges since v2:\n\n * the series is now only about the working tree. So if the original cwd is\n   outside the worktree (or we're in a bare repo), then the new code is a\n   no-op.\n * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n   xgetcwd())\n * modified the initial tests to show both expected and desired behavior.\n   subsequent patches fix the tests. One new patch added at the end which\n   simplifies the tests to only check for desired behavior.\n * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n   that is already protected and we don't need secondary protection for it.\n   This simplified some other codepaths so we don't have to check for\n   startup_info->original_cwd == \"\".\n * clarified some commit messages\n\nChanges since v1:\n\n * clarified multiple commit messages\n * renamed the_cwd to startup_info->original_cwd to make it clearer that\n   it's our parent process'es cwd that really matters, which we inherited at\n   program startup. Also pulls it out of the global namespace.\n * Normalize the path for startup_info->original_cwd, and ensure that it's\n   actually the original cwd even if -C is passed to git.\n * small code cleanups suggested by René and Ævar\n * split the final patch (which got the most comments) into two -- one for\n   each function being modified. Also, add a bunch more history to the first\n   of the two resulting commit messages\n * no longer has a content conflict with so/stash-staged\n * add another value for the flags parameter that remove_dir_recursively()\n   takes so that it can opt into either the old or the new behavior. Use\n   that for the one special corner case I could find where it matters, and\n   add a few tests around it to highlight the utility of the flag.\n\nElijah Newren (11):\n  t2501: add various tests for removing the current working directory\n  setup: introduce startup_info->original_cwd\n  unpack-trees: refuse to remove startup_info->original_cwd\n  unpack-trees: add special cwd handling\n  symlinks: do not include startup_info->original_cwd in dir removal\n  clean: do not attempt to remove startup_info->original_cwd\n  rebase: do not attempt to remove startup_info->original_cwd\n  stash: do not attempt to remove startup_info->original_cwd\n  dir: avoid incidentally removing the original_cwd in remove_path()\n  dir: new flag to remove_dir_recurse() to spare the original_cwd\n  t2501: simplify the tests since we can now assume desired behavior\n\n builtin/clean.c      |  44 +++++--\n builtin/rm.c         |   3 +-\n builtin/stash.c      |   4 +-\n cache.h              |   2 +\n common-main.c        |   4 +\n dir.c                |  15 ++-\n dir.h                |   9 +-\n sequencer.c          |   2 +\n setup.c              |  65 ++++++++++\n symlinks.c           |   8 +-\n t/t2501-cwd-empty.sh | 277 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  30 ++++-\n unpack-trees.h       |   1 +\n 13 files changed, 442 insertions(+), 22 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v4\nPull-Request: https://github.com/git/git/pull/1140\n\nRange-diff vs v3:\n\n  1:  4b0044656b0 !  1:  a45b3f08802 t2501: add various tests for removing the current working directory\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t\t# Although we want pwd & git status to pass, test for existing\n      +\t\t# rather than desired behavior.\n     -+\t\tif [[ $works == \"success\" ]]; then\n     -+\t\t\tpwd -P &&\n     ++\t\tif test \"$works\" == \"success\"\n     ++\t\tthen\n     ++\t\t\ttest-tool getcwd &&\n      +\t\t\tgit status --porcelain\n      +\t\telse\n     -+\t\t\t! pwd -P &&\n     ++\t\t\t! test-tool getcwd &&\n      +\t\t\ttest_might_fail git status --porcelain\n      +\t\tfi\n      +\t) &&\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t# Although we want dir to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif [[ $works == \"success\" ]]; then\n     ++\tif test \"$works\" == \"success\"\n     ++\tthen\n      +\t\ttest_path_is_dir foo\n      +\telse\n      +\t\ttest_path_is_missing foo\n     @@ t/t2501-cwd-empty.sh (new)\n      +\t\tcd dirORfile &&\n      +\n      +\t\t# We'd like for the command to fail (much as it would if there\n     -+\t\t# was an untracked file there), and for pwd & git status to\n     -+\t\t# succeed afterwards.  But test for existing rather than\n     -+\t\t# desired behavior.\n     -+\t\tif [[ $works == \"success\" ]]; then\n     ++\t\t# was an untracked file there), and for the index and worktree\n     ++\t\t# to be left clean with pwd and git status working afterwards.\n     ++\t\t# But test for existing rather than desired behavior.\n     ++\t\tif test \"$works\" == \"success\"\n     ++\t\tthen\n      +\t\t\ttest_must_fail \"$@\" 2>../error &&\n      +\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     -+\t\t\tpwd -P &&\n     ++\n     ++\t\t\tgit diff --exit-code HEAD &&\n     ++\n     ++\t\t\ttest-tool getcwd &&\n      +\t\t\tgit status --porcelain\n      +\t\telse\n      +\t\t\t\"$@\" &&\n     -+\t\t\t! pwd -P &&\n     ++\t\t\t! test-tool getcwd &&\n      +\t\t\ttest_might_fail git status --porcelain\n      +\t\tfi\n      +\t) &&\n      +\n      +\t# Although we want dirORfile to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif [[ $works == \"success\" ]]; then\n     ++\tif test \"$works\" == \"success\"\n     ++\tthen\n      +\t\ttest_path_is_dir dirORfile\n      +\telse\n      +\t\ttest_path_is_file dirORfile\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t\t# Although we want pwd & git status to pass, test for existing\n      +\t\t# rather than desired behavior.\n     -+\t\tif [[ $works == \"success\" ]]; then\n     -+\t\t\tpwd -P &&\n     ++\t\tif test \"$works\" == \"success\"\n     ++\t\tthen\n     ++\t\t\ttest-tool getcwd &&\n      +\t\t\tgit status --porcelain\n      +\t\telse\n     -+\t\t\t! pwd -P &&\n     ++\t\t\t! test-tool getcwd &&\n      +\t\t\ttest_might_fail git status --porcelain\n      +\t\tfi\n      +\t) &&\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t# Although we want dir to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif [[ $works == \"success\" ]]; then\n     ++\tif test \"$works\" == \"success\"\n     ++\tthen\n      +\t\ttest_path_is_dir untracked\n      +\telse\n      +\t\ttest_path_is_missing untracked\n     @@ t/t2501-cwd-empty.sh (new)\n      +\tshift &&\n      +\n      +\ttest_status=\n     -+\ttest $path_status = dir && test_status=test_must_fail\n     ++\ttest \"$path_status\" = dir && test_status=test_must_fail\n      +\n      +\t# Actually, while path_status == dir && test_status=test_must_fail\n      +\t# reflect our desired behavior, current behavior is:\n  2:  200ddece05d !  2:  ca9f632bd11 setup: introduce startup_info->original_cwd\n     @@ setup.c: void setup_work_tree(void)\n      +\t\tstartup_info->original_cwd = \\\n      +\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n      +\t\t\t\t\t\t    + offset);\n     ++\t\treturn;\n      +\t}\n     -+\treturn;\n      +\n      +no_prevention_needed:\n      +\tfree((char*)startup_info->original_cwd);\n  3:  68ae90546fe =  3:  41a82eff41e unpack-trees: refuse to remove startup_info->original_cwd\n  4:  1bb8905900c =  4:  2e2ea02f97b unpack-trees: add special cwd handling\n  5:  8a33d74e7cf =  5:  f444a541da4 symlinks: do not include startup_info->original_cwd in dir removal\n  6:  11e4ec881bb =  6:  1990e36bb41 clean: do not attempt to remove startup_info->original_cwd\n  7:  39b1f3a225e !  7:  1035ee7f9ce rebase: do not attempt to remove startup_info->original_cwd\n     @@ sequencer.c: static int run_git_checkout(struct repository *r, struct replay_opt\n       \n       \tcmd.git_cmd = 1;\n       \n     -+\tif (startup_info->original_cwd &&\n     -+\t    !is_absolute_path(startup_info->original_cwd))\n     ++\tif (startup_info->original_cwd)\n      +\t\tcmd.dir = startup_info->original_cwd;\n       \tstrvec_push(&cmd.args, \"checkout\");\n       \tstrvec_push(&cmd.args, commit);\n  8:  0110462a19c !  8:  a2be40a22d1 stash: do not attempt to remove startup_info->original_cwd\n     @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char\n       \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n       \n       \t\t\tcp.git_cmd = 1;\n     -+\t\t\tif (startup_info->original_cwd &&\n     -+\t\t\t    !is_absolute_path(startup_info->original_cwd))\n     ++\t\t\tif (startup_info->original_cwd)\n      +\t\t\t\tcp.dir = startup_info->original_cwd;\n       \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n      -\t\t\t\t     \"--quiet\", \"-d\", NULL);\n  9:  2c73a09a2e8 =  9:  834031be9e0 dir: avoid incidentally removing the original_cwd in remove_path()\n 10:  d4e50b4053d ! 10:  d5750fcb6d5 dir: new flag to remove_dir_recurse() to spare the original_cwd\n     @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n       ## t/t2501-cwd-empty.sh ##\n      @@ t/t2501-cwd-empty.sh: test_submodule_removal () {\n       \ttest_status=\n     - \ttest $path_status = dir && test_status=test_must_fail\n     + \ttest \"$path_status\" = dir && test_status=test_must_fail\n       \n      -\t# Actually, while path_status == dir && test_status=test_must_fail\n      -\t# reflect our desired behavior, current behavior is:\n 11:  7eb6281be4b ! 11:  21ff99a767c t2501: simplify the tests since we can now assume desired behavior\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \n      -\t\t# Although we want pwd & git status to pass, test for existing\n      -\t\t# rather than desired behavior.\n     --\t\tif [[ $works == \"success\" ]]; then\n     --\t\t\tpwd -P &&\n     +-\t\tif test \"$works\" == \"success\"\n     +-\t\tthen\n     +-\t\t\ttest-tool getcwd &&\n      -\t\t\tgit status --porcelain\n      -\t\telse\n     --\t\t\t! pwd -P &&\n     +-\t\t\t! test-tool getcwd &&\n      -\t\t\ttest_might_fail git status --porcelain\n      -\t\tfi\n     -+\t\tpwd -P &&\n     ++\t\t# Make sure foo still exists, and commands needing it work\n     ++\t\ttest-tool getcwd &&\n      +\t\tgit status --porcelain\n       \t) &&\n       \ttest_path_is_missing foo/bar/baz &&\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \n      -\t# Although we want dir to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif [[ $works == \"success\" ]]; then\n     +-\tif test \"$works\" == \"success\"\n     +-\tthen\n      -\t\ttest_path_is_dir foo\n      -\telse\n      -\t\ttest_path_is_missing foo\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \t\tcd dirORfile &&\n       \n      -\t\t# We'd like for the command to fail (much as it would if there\n     --\t\t# was an untracked file there), and for pwd & git status to\n     --\t\t# succeed afterwards.  But test for existing rather than\n     --\t\t# desired behavior.\n     --\t\tif [[ $works == \"success\" ]]; then\n     +-\t\t# was an untracked file there), and for the index and worktree\n     +-\t\t# to be left clean with pwd and git status working afterwards.\n     +-\t\t# But test for existing rather than desired behavior.\n     +-\t\tif test \"$works\" == \"success\"\n     +-\t\tthen\n      -\t\t\ttest_must_fail \"$@\" 2>../error &&\n      -\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     --\t\t\tpwd -P &&\n     +-\n     +-\t\t\tgit diff --exit-code HEAD &&\n     +-\n     +-\t\t\ttest-tool getcwd &&\n      -\t\t\tgit status --porcelain\n      -\t\telse\n      -\t\t\t\"$@\" &&\n     --\t\t\t! pwd -P &&\n     +-\t\t\t! test-tool getcwd &&\n      -\t\t\ttest_might_fail git status --porcelain\n      -\t\tfi\n     ++\t\t# Ensure command refuses to run\n      +\t\ttest_must_fail \"$@\" 2>../error &&\n      +\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     -+\t\tpwd -P &&\n     ++\n     ++\t\t# ...and that the index and working tree are left clean\n     ++\t\tgit diff --exit-code HEAD &&\n     ++\n     ++\t\t# Ensure that getcwd and git status do not error out (which\n     ++\t\t# they might if the current working directory had been removed)\n     ++\t\ttest-tool getcwd &&\n      +\t\tgit status --porcelain\n       \t) &&\n       \n      -\t# Although we want dirORfile to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif [[ $works == \"success\" ]]; then\n     +-\tif test \"$works\" == \"success\"\n     +-\tthen\n      -\t\ttest_path_is_dir dirORfile\n      -\telse\n      -\t\ttest_path_is_file dirORfile\n     @@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n       \n      -\t\t# Although we want pwd & git status to pass, test for existing\n      -\t\t# rather than desired behavior.\n     --\t\tif [[ $works == \"success\" ]]; then\n     --\t\t\tpwd -P &&\n     +-\t\tif test \"$works\" == \"success\"\n     +-\t\tthen\n     +-\t\t\ttest-tool getcwd &&\n      -\t\t\tgit status --porcelain\n      -\t\telse\n     --\t\t\t! pwd -P &&\n     +-\t\t\t! test-tool getcwd &&\n      -\t\t\ttest_might_fail git status --porcelain\n      -\t\tfi\n     -+\t\tpwd -P &&\n     ++\t\t# Make sure untracked still exists, and commands needing it work\n     ++\t\ttest-tool getcwd &&\n      +\t\tgit status --porcelain\n       \t) &&\n       \ttest_path_is_missing empty &&\n     @@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n       \n      -\t# Although we want dir to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif [[ $works == \"success\" ]]; then\n     +-\tif test \"$works\" == \"success\"\n     +-\tthen\n      -\t\ttest_path_is_dir untracked\n      -\telse\n      -\t\ttest_path_is_missing untracked\n\n-- \ngitgitgadget\n"},{"id":"442571","messageId":"21ff99a767ce2bbb79b3ec22a338ab19cd088ef3.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 11/11] t2501: simplify the tests since we can now assume desired behavior","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:14Z","receivedAt":"2021-11-29T23:10:30Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe no longer are dealing with a mixture of previous and desired\nbehavior, so simplify the tests a bit.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------\n 1 file changed, 36 insertions(+), 87 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 6d8f68c08dd..f6d8d7d03d7 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -32,9 +32,6 @@ test_expect_success setup '\n '\n \n test_incidental_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -44,88 +41,57 @@ test_incidental_dir_removal () {\n \t\tcd foo &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" == \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure foo still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" == \"success\"\n-\tthen\n-\t\ttest_path_is_dir foo\n-\telse\n-\t\ttest_path_is_missing foo\n-\tfi\n+\ttest_path_is_dir foo\n }\n \n test_required_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \tgit checkout df_conflict^{commit} &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We'd like for the command to fail (much as it would if there\n-\t\t# was an untracked file there), and for the index and worktree\n-\t\t# to be left clean with pwd and git status working afterwards.\n-\t\t# But test for existing rather than desired behavior.\n-\t\tif test \"$works\" == \"success\"\n-\t\tthen\n-\t\t\ttest_must_fail \"$@\" 2>../error &&\n-\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n-\n-\t\t\tgit diff --exit-code HEAD &&\n-\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t\"$@\" &&\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Ensure command refuses to run\n+\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t# ...and that the index and working tree are left clean\n+\t\tgit diff --exit-code HEAD &&\n+\n+\t\t# Ensure that getcwd and git status do not error out (which\n+\t\t# they might if the current working directory had been removed)\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \n-\t# Although we want dirORfile to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" == \"success\"\n-\tthen\n-\t\ttest_path_is_dir dirORfile\n-\telse\n-\t\ttest_path_is_file dirORfile\n-\tfi\n+\ttest_path_is_dir dirORfile\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git checkout init\n+\ttest_incidental_dir_removal git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git checkout fd_conflict\n+\ttest_required_dir_removal git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git reset --hard init\n+\ttest_incidental_dir_removal git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git reset --hard fd_conflict\n+\ttest_required_dir_removal git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git merge reverted\n+\ttest_incidental_dir_removal git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -154,46 +120,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git merge fd_conflict\n+\ttest_required_dir_removal git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git cherry-pick reverted\n+\ttest_incidental_dir_removal git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git cherry-pick fd_conflict\n+\ttest_required_dir_removal git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rebase reverted\n+\ttest_incidental_dir_removal git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git rebase fd_conflict\n+\ttest_required_dir_removal git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git revert HEAD\n+\ttest_incidental_dir_removal git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git revert undo_fd_conflict\n+\ttest_required_dir_removal git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rm bar/baz.t\n+\ttest_incidental_dir_removal git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal success git apply ../patch\n+\ttest_incidental_dir_removal git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -205,38 +168,24 @@ test_incidental_untracked_dir_removal () {\n \t\tcd untracked &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" == \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure untracked still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing empty &&\n \ttest_path_is_missing untracked/random &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" == \"success\"\n-\tthen\n-\t\ttest_path_is_dir untracked\n-\telse\n-\t\ttest_path_is_missing untracked\n-\tfi\n+\ttest_path_is_dir untracked\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n \tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n"},{"id":"442572","messageId":"ca9f632bd119d6aa507976e35dc0d1b178fdeea7.1638225434.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v4 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-11-29T22:37:05Z","receivedAt":"2021-11-29T23:10:31Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands run from that directory to get confused and fail with a message\nabout being unable to read the current working directory:\n\n    $ git status\n    fatal: Unable to read current working directory: No such file or directory\n\nNon-git commands likely have similar warnings or even errors, e.g.\n\n    $ bash -c 'echo hello'\n    shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n    hello\n\nThis confuses end users, particularly since the command they get the\nerror from is not the one that caused the problem; the problem came from\nthe side-effect of some previous command.\n\nWe would like to avoid removing the current working directory of our\nparent process; towards this end, introduce a new variable,\nstartup_info->original_cwd, that tracks the current working directory\nthat we inherited from our parent process.  For convenience of later\ncomparisons, we prefer that this new variable store a path relative to\nthe toplevel working directory (thus much like 'prefix'), except without\nthe trailing slash.\n\nSubsequent commits will make use of this new variable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n cache.h       |  2 ++\n common-main.c |  4 ++++\n setup.c       | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex eba12487b99..92e181ea759 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1834,8 +1834,10 @@ void overlay_tree_on_index(struct index_state *istate,\n struct startup_info {\n \tint have_repository;\n \tconst char *prefix;\n+\tconst char *original_cwd;\n };\n extern struct startup_info *startup_info;\n+extern const char *tmp_original_cwd;\n \n /* merge.c */\n struct commit_list;\ndiff --git a/common-main.c b/common-main.c\nindex 71e21dd20a3..aa8d5aba5bb 100644\n--- a/common-main.c\n+++ b/common-main.c\n@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)\n int main(int argc, const char **argv)\n {\n \tint result;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \n \ttrace2_initialize_clock();\n \n@@ -49,6 +50,9 @@ int main(int argc, const char **argv)\n \ttrace2_cmd_start(argv);\n \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n \n+\tif (!strbuf_getcwd(&tmp))\n+\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n+\n \tresult = cmd_main(argc, argv);\n \n \ttrace2_cmd_exit(result);\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..af3b8c09abe 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -12,6 +12,7 @@ static int work_tree_config_is_bogus;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n+const char *tmp_original_cwd;\n \n /*\n  * The input parameter must contain an absolute path, and it must already be\n@@ -432,6 +433,69 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static void setup_original_cwd(void)\n+{\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tconst char *worktree = NULL;\n+\tint offset = -1;\n+\n+\tif (!tmp_original_cwd)\n+\t\treturn;\n+\n+\t/*\n+\t * startup_info->original_cwd points to the current working\n+\t * directory we inherited from our parent process, which is a\n+\t * directory we want to avoid removing.\n+\t *\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n+\t *\n+\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+\t * but differs in two ways:\n+\t *   - prefix has a trailing '/'\n+\t *   - if the user passes '-C' to git, that modifies the prefix but\n+\t *     not startup_info->original_cwd.\n+\t */\n+\n+\t/* Normalize the directory */\n+\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n+\tfree((char*)tmp_original_cwd);\n+\ttmp_original_cwd = NULL;\n+\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n+\n+\t/*\n+\t * Get our worktree; we only protect the current working directory\n+\t * if it's in the worktree.\n+\t */\n+\tworktree = get_git_work_tree();\n+\tif (!worktree)\n+\t\tgoto no_prevention_needed;\n+\n+\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n+\tif (offset >= 0) {\n+\t\t/*\n+\t\t * If startup_info->original_cwd == worktree, that is already\n+\t\t * protected and we don't need original_cwd as a secondary\n+\t\t * protection measure.\n+\t\t */\n+\t\tif (!*(startup_info->original_cwd + offset))\n+\t\t\tgoto no_prevention_needed;\n+\n+\t\t/*\n+\t\t * original_cwd was inside worktree; precompose it just as\n+\t\t * we do prefix so that built up paths will match\n+\t\t */\n+\t\tstartup_info->original_cwd = \\\n+\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n+\t\t\t\t\t\t    + offset);\n+\t\treturn;\n+\t}\n+\n+no_prevention_needed:\n+\tfree((char*)startup_info->original_cwd);\n+\tstartup_info->original_cwd = NULL;\n+}\n+\n static int read_worktree_config(const char *var, const char *value, void *vdata)\n {\n \tstruct repository_format *data = vdata;\n@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n+\tsetup_original_cwd();\n \n \tstrbuf_release(&dir);\n \tstrbuf_release(&gitdir);\n-- \ngitgitgadget\n\n"},{"id":"442582","messageId":"CAPig+cToMeDdO2jnf18A8gPOZy_Jc=yN-uPTmBSuYJA6O+RMTA@mail.gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-11-29T23:38:46Z","receivedAt":"2021-11-29T23:38:59Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Nov 29, 2021 at 6:10 PM Elijah Newren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n> Changes since v3:\n>  * fixed bashism\n>      -+         if [[ $works == \"success\" ]]; then\n>      ++         if test \"$works\" == \"success\"\n\nThis and any other instances in the series should be using POSIX `=`,\nnot Bash `==`.\n"},{"id":"442589","messageId":"CABPp-BFTYNGeEoxnnuoz_8YiVfGf-fd2Du2g26yOCJ8uTDy-hQ@mail.gmail.com","threadId":"56952","inReplyTo":"CAPig+cToMeDdO2jnf18A8gPOZy_Jc=yN-uPTmBSuYJA6O+RMTA@mail.gmail.com","subject":"Re: [PATCH v4 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-30T00:16:20Z","receivedAt":"2021-11-30T00:16:34Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 3:38 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>\n> On Mon, Nov 29, 2021 at 6:10 PM Elijah Newren via GitGitGadget\n> <gitgitgadget@gmail.com> wrote:\n> > Changes since v3:\n> >  * fixed bashism\n> >      -+         if [[ $works == \"success\" ]]; then\n> >      ++         if test \"$works\" == \"success\"\n>\n> This and any other instances in the series should be using POSIX `=`,\n> not Bash `==`.\n\nUgh, I had them removed by the end of the series but somehow missed\nchecking the earlier patch.  I'll fix it up; thanks for spotting.\n"},{"id":"442609","messageId":"xmqqilwa2l8f.fsf@gitster.g","threadId":"56952","inReplyTo":"a45b3f088025795d11a78055f90b8632d435d74f.1638225434.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 01/11] t2501: add various tests for removing the current working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-11-30T06:47:28Z","receivedAt":"2021-11-30T06:47:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +\t\t# Although we want pwd & git status to pass, test for existing\n> +\t\t# rather than desired behavior.\n> +\t\tif test \"$works\" == \"success\"\n\nDon't double \"=\" here.  We are not writing for bash.  There are a\nfew more instances of the same mistake in this patch.\n"},{"id":"442610","messageId":"CABPp-BHQsQqjCwjCUk-bSe_i52Wza-QgTDbHVsLn90D8q5FkuQ@mail.gmail.com","threadId":"56952","inReplyTo":"xmqqilwa2l8f.fsf@gitster.g","subject":"Re: [PATCH v4 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-11-30T06:53:10Z","receivedAt":"2021-11-30T06:53:24Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 29, 2021 at 10:47 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > +             # Although we want pwd & git status to pass, test for existing\n> > +             # rather than desired behavior.\n> > +             if test \"$works\" == \"success\"\n>\n> Don't double \"=\" here.  We are not writing for bash.  There are a\n> few more instances of the same mistake in this patch.\n\nYep, also pointed out by Eric.  I've got a v5 queued up whose change\nis just fixing this, but I'm waiting a bit to see if other items are\nspotted in review before sending.\n\nThanks for spotting.\n"},{"id":"442624","messageId":"8e8b95d7-a4b4-6839-c572-8a43bbae8e0c@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v3.git.git.1637966463.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2021-11-30T11:04:32Z","receivedAt":"2021-11-30T11:04:38Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Elijah\n\nOn 26/11/2021 22:40, Elijah Newren via GitGitGadget wrote:\n> Traditionally, if folks run git commands such as checkout or rebase from a\n> subdirectory, that git command could remove their current working directory\n> and result in subsequent git and non-git commands either getting confused or\n> printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> working directory: No such file or directory\"). Many commands either\n> silently avoid removing directories that are not empty (i.e. those that have\n> untracked or modified files in them)[1], or show an error and abort,\n> depending on which is more appropriate for the command in question. With\n> this series, we augment the reasons to avoid removing directories to include\n> not just has-untracked-or-modified-files, but also to avoid removing the\n> original_cwd as well.\n> \n> Peff and Junio provided some good pros/cons, if it helps:\n> \n>   * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n>   * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n> \n> [1] well, with a few exceptions; see\n> https://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n> [2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n> [3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\n> https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\n\nThanks for working on this, I'm sorry I haven't had time to take a \nproper look at it but I think it is a good idea. I did notice that \nyou're using \":/\" in patch 8 and wondered what happens if some runs 'git \n--literal-pathspecs stash' but I haven't looked properly.\n\nBest Wishes\n\nPhillip\n\n> Changes since v2:\n> \n>   * the series is now only about the working tree. So if the original cwd is\n>     outside the worktree (or we're in a bare repo), then the new code is a\n>     no-op.\n>   * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n>     xgetcwd())\n>   * modified the initial tests to show both expected and desired behavior.\n>     subsequent patches fix the tests. One new patch added at the end which\n>     simplifies the tests to only check for desired behavior.\n>   * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n>     that is already protected and we don't need secondary protection for it.\n>     This simplified some other codepaths so we don't have to check for\n>     startup_info->original_cwd == \"\".\n>   * clarified some commit messages\n> \n> Changes since v1:\n> \n>   * clarified multiple commit messages\n>   * renamed the_cwd to startup_info->original_cwd to make it clearer that\n>     it's our parent process'es cwd that really matters, which we inherited at\n>     program startup. Also pulls it out of the global namespace.\n>   * Normalize the path for startup_info->original_cwd, and ensure that it's\n>     actually the original cwd even if -C is passed to git.\n>   * small code cleanups suggested by René and Ævar\n>   * split the final patch, which got the most comments into two, one for each\n>     function being modified; significantly extending the first of the two\n>     commit messages with a lot of history\n>   * no longer has a content conflict with so/stash-staged\n>   * add another value for the flags parameter that remove_dir_recursively()\n>     takes so that it can opt into either the old or the new behavior. Use\n>     that for the one special corner case I could find where it matters, and\n>     add a few tests around it to highlight the utility of the flag.\n> \n> Elijah Newren (11):\n>    t2501: add various tests for removing the current working directory\n>    setup: introduce startup_info->original_cwd\n>    unpack-trees: refuse to remove startup_info->original_cwd\n>    unpack-trees: add special cwd handling\n>    symlinks: do not include startup_info->original_cwd in dir removal\n>    clean: do not attempt to remove startup_info->original_cwd\n>    rebase: do not attempt to remove startup_info->original_cwd\n>    stash: do not attempt to remove startup_info->original_cwd\n>    dir: avoid incidentally removing the original_cwd in remove_path()\n>    dir: new flag to remove_dir_recurse() to spare the original_cwd\n>    t2501: simplify the tests since we can now assume desired behavior\n> \n>   builtin/clean.c      |  44 +++++--\n>   builtin/rm.c         |   3 +-\n>   builtin/stash.c      |   5 +-\n>   cache.h              |   2 +\n>   common-main.c        |   4 +\n>   dir.c                |  15 ++-\n>   dir.h                |   9 +-\n>   sequencer.c          |   3 +\n>   setup.c              |  65 +++++++++++\n>   symlinks.c           |   8 +-\n>   t/t2501-cwd-empty.sh | 268 +++++++++++++++++++++++++++++++++++++++++++\n>   unpack-trees.c       |  30 ++++-\n>   unpack-trees.h       |   1 +\n>   13 files changed, 435 insertions(+), 22 deletions(-)\n>   create mode 100755 t/t2501-cwd-empty.sh\n> \n> \n> base-commit: 88d915a634b449147855041d44875322de2b286d\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v3\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v3\n> Pull-Request: https://github.com/git/git/pull/1140\n> \n> Range-diff vs v2:\n> \n>    1:  38a120f5c03 !  1:  4b0044656b0 t2501: add various tests for removing the current working directory\n>       @@ Metadata\n>         ## Commit message ##\n>            t2501: add various tests for removing the current working directory\n>        \n>       -    Numerous commands will remove empty working directories, especially if\n>       -    they are in the way of placing needed files.  That is normally fine, but\n>       -    removing the current working directory can cause confusion for the user\n>       -    when they run subsequent commands.  For example, after one git process\n>       -    has removed the current working directory, git status/log/diff will all\n>       -    abort with the message:\n>       +    Numerous commands will remove directories left empty as a \"convenience\"\n>       +    after removing files within them.  That is normally fine, but removing\n>       +    the current working directory can be rather inconvenient since it can\n>       +    cause confusion for the user when they run subsequent commands.  For\n>       +    example, after one git process has removed the current working\n>       +    directory, git status/log/diff will all abort with the message:\n>        \n>                fatal: Unable to read current working directory: No such file or directory\n>        \n>       +    We also have code paths that, when a file needs to be placed where a\n>       +    directory is (due to e.g. checkout, merge, reset, whatever), will check\n>       +    if this is okay and error out if not.  These rules include:\n>       +      * all tracked files under that directory are intended to be removed by\n>       +        the operation\n>       +      * none of the tracked files under that directory have uncommitted\n>       +        modification\n>       +      * there are no untracked files under that directory\n>       +    However, if we end up remove the current working directory, we can cause\n>       +    user confusion when they run subsequent commands, so we would prefer if\n>       +    there was a fourth rule added to this list: avoid removing the current\n>       +    working directory.\n>       +\n>            Since there are several code paths that can result in the current\n>            working directory being removed, add several tests of various different\n>       -    codepaths that check for the behavior we would instead like to see.\n>       -    This include a number of new error messages that we will be adding in\n>       -    subsequent commits as we implement the desired checks.\n>       +    codepaths.  To make it clearer what the difference between the current\n>       +    behavior and the behavior at the end of the series, code both of them\n>       +    into the tests and have the appropriate behavior be selected by a flag.\n>       +    Subsequent commits will toggle the flag from current to desired\n>       +    behavior.\n>       +\n>       +    Also add a few tests suggested during the review of earlier rounds of\n>       +    this patch series.\n>        \n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>       @@ t/t2501-cwd-empty.sh (new)\n>        +\n>        +test_expect_success setup '\n>        +\ttest_commit init &&\n>       -+\tmkdir subdir &&\n>       -+\ttest_commit subdir/file &&\n>        +\n>        +\tgit branch fd_conflict &&\n>        +\n>       @@ t/t2501-cwd-empty.sh (new)\n>        +\tgit tag reverted &&\n>        +\n>        +\tgit checkout fd_conflict &&\n>       -+\tgit rm subdir/file.t &&\n>       ++\tmkdir dirORfile &&\n>       ++\ttest_commit dirORfile/foo &&\n>       ++\n>       ++\tgit rm -r dirORfile &&\n>        +\techo not-a-directory >dirORfile &&\n>        +\tgit add dirORfile &&\n>       -+\tgit commit -m dirORfile\n>       ++\tgit commit -m dirORfile &&\n>       ++\n>       ++\tgit switch -c df_conflict HEAD~1 &&\n>       ++\ttest_commit random_file &&\n>       ++\n>       ++\tgit switch -c undo_fd_conflict fd_conflict &&\n>       ++\tgit revert HEAD\n>        +'\n>        +\n>       -+test_expect_failure 'checkout does not clean cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       ++test_incidental_dir_removal () {\n>       ++\tworks=$1 &&\n>       ++\tshift &&\n>       ++\n>       ++\ttest_when_finished \"git reset --hard\" &&\n>       ++\n>       ++\tgit checkout foo/bar/baz^{commit} &&\n>        +\ttest_path_is_dir foo/bar &&\n>        +\n>        +\t(\n>        +\t\tcd foo &&\n>       -+\t\tgit checkout init &&\n>       -+\t\tcd ..\n>       ++\t\t\"$@\" &&\n>       ++\n>       ++\t\t# Although we want pwd & git status to pass, test for existing\n>       ++\t\t# rather than desired behavior.\n>       ++\t\tif [[ $works == \"success\" ]]; then\n>       ++\t\t\tpwd -P &&\n>       ++\t\t\tgit status --porcelain\n>       ++\t\telse\n>       ++\t\t\t! pwd -P &&\n>       ++\t\t\ttest_might_fail git status --porcelain\n>       ++\t\tfi\n>        +\t) &&\n>        +\ttest_path_is_missing foo/bar/baz &&\n>        +\ttest_path_is_missing foo/bar &&\n>       -+\ttest_path_is_dir foo\n>       -+'\n>        +\n>       -+test_expect_failure 'checkout fails if cwd needs to be removed' '\n>       -+\tgit checkout foo/bar/baz &&\n>       ++\t# Although we want dir to be present, test for existing rather\n>       ++\t# than desired behavior.\n>       ++\tif [[ $works == \"success\" ]]; then\n>       ++\t\ttest_path_is_dir foo\n>       ++\telse\n>       ++\t\ttest_path_is_missing foo\n>       ++\tfi\n>       ++}\n>       ++\n>       ++test_required_dir_removal () {\n>       ++\tworks=$1 &&\n>       ++\tshift &&\n>       ++\n>       ++\tgit checkout df_conflict^{commit} &&\n>        +\ttest_when_finished \"git clean -fdx\" &&\n>        +\n>       -+\tmkdir dirORfile &&\n>        +\t(\n>        +\t\tcd dirORfile &&\n>        +\n>       -+\t\ttest_must_fail git checkout fd_conflict 2>../error &&\n>       -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n>       ++\t\t# We'd like for the command to fail (much as it would if there\n>       ++\t\t# was an untracked file there), and for pwd & git status to\n>       ++\t\t# succeed afterwards.  But test for existing rather than\n>       ++\t\t# desired behavior.\n>       ++\t\tif [[ $works == \"success\" ]]; then\n>       ++\t\t\ttest_must_fail \"$@\" 2>../error &&\n>       ++\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n>       ++\t\t\tpwd -P &&\n>       ++\t\t\tgit status --porcelain\n>       ++\t\telse\n>       ++\t\t\t\"$@\" &&\n>       ++\t\t\t! pwd -P &&\n>       ++\t\t\ttest_might_fail git status --porcelain\n>       ++\t\tfi\n>        +\t) &&\n>        +\n>       -+\ttest_path_is_dir dirORfile\n>       ++\t# Although we want dirORfile to be present, test for existing rather\n>       ++\t# than desired behavior.\n>       ++\tif [[ $works == \"success\" ]]; then\n>       ++\t\ttest_path_is_dir dirORfile\n>       ++\telse\n>       ++\t\ttest_path_is_file dirORfile\n>       ++\tfi\n>       ++}\n>       ++\n>       ++test_expect_success 'checkout does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git checkout init\n>        +'\n>        +\n>       -+test_expect_failure 'reset --hard does not clean cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_path_is_dir foo/bar &&\n>       ++test_expect_success 'checkout fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git checkout fd_conflict\n>       ++'\n>        +\n>       -+\t(\n>       -+\t\tcd foo &&\n>       -+\t\tgit reset --hard init &&\n>       -+\t\tcd ..\n>       -+\t) &&\n>       -+\ttest_path_is_missing foo/bar/baz &&\n>       -+\ttest_path_is_missing foo/bar &&\n>       -+\ttest_path_is_dir foo\n>       ++test_expect_success 'reset --hard does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git reset --hard init\n>       ++'\n>       ++\n>       ++test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git reset --hard fd_conflict\n>       ++'\n>       ++\n>       ++test_expect_success 'merge does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git merge reverted\n>        +'\n>        +\n>       -+test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n>       ++# This file uses some simple merges where\n>       ++#   Base: 'dirORfile/' exists\n>       ++#   Side1: random other file changed\n>       ++#   Side2: 'dirORfile/' removed, 'dirORfile' added\n>       ++# this should resolve cleanly, but merge-recursive throws merge conflicts\n>       ++# because it's dumb.  Add a special test for checking merge-recursive (and\n>       ++# merge-ort), then after this just hard require ort for all remaining tests.\n>       ++#\n>       ++test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n>        +\tgit checkout foo/bar/baz &&\n>        +\ttest_when_finished \"git clean -fdx\" &&\n>        +\n>       @@ t/t2501-cwd-empty.sh (new)\n>        +\t(\n>        +\t\tcd dirORfile &&\n>        +\n>       -+\t\ttest_must_fail git reset --hard fd_conflict 2>../error &&\n>       -+\t\tgrep \"Refusing to remove.*the current working directory\" ../error\n>       ++\t\t# We would rather this failed, but we test for existing\n>       ++\t\t# rather than desired behavior\n>       ++\t\tgit merge fd_conflict 2>../error\n>        +\t) &&\n>        +\n>       -+\ttest_path_is_dir dirORfile\n>       ++\t## Here is the behavior we would rather have:\n>       ++\t#test_path_is_dir dirORfile &&\n>       ++\t#grep \"Refusing to remove the current working directory\" error\n>       ++\t## But instead we test for existing behavior\n>       ++\ttest_path_is_file dirORfile &&\n>       ++\ttest_must_be_empty error\n>        +'\n>        +\n>       -+test_expect_failure 'merge does not remove cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++GIT_TEST_MERGE_ALGORITHM=ort\n>        +\n>       -+\t(\n>       -+\t\tcd subdir &&\n>       -+\t\tgit merge fd_conflict\n>       -+\t) &&\n>       ++test_expect_success 'merge fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git merge fd_conflict\n>       ++'\n>        +\n>       -+\ttest_path_is_missing subdir/file.t &&\n>       -+\ttest_path_is_dir subdir\n>       ++test_expect_success 'cherry-pick does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git cherry-pick reverted\n>        +'\n>        +\n>       -+test_expect_failure 'merge fails if cwd needs to be removed' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git cherry-pick fd_conflict\n>       ++'\n>        +\n>       -+\tmkdir dirORfile &&\n>       -+\t(\n>       -+\t\tcd dirORfile &&\n>       -+\t\ttest_must_fail git merge fd_conflict 2>../error &&\n>       -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n>       -+\t) &&\n>       ++test_expect_success 'rebase does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git rebase reverted\n>       ++'\n>        +\n>       -+\ttest_path_is_dir dirORfile\n>       ++test_expect_success 'rebase fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git rebase fd_conflict\n>        +'\n>        +\n>       -+test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success 'revert does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git revert HEAD\n>       ++'\n>        +\n>       -+\t(\n>       -+\t\tcd subdir &&\n>       -+\t\tgit cherry-pick fd_conflict\n>       -+\t) &&\n>       ++test_expect_success 'revert fails if cwd needs to be removed' '\n>       ++\ttest_required_dir_removal failure git revert undo_fd_conflict\n>       ++'\n>        +\n>       -+\ttest_path_is_missing subdir/file.t &&\n>       -+\ttest_path_is_dir subdir\n>       ++test_expect_success 'rm does not clean cwd incidentally' '\n>       ++\ttest_incidental_dir_removal failure git rm bar/baz.t\n>        +'\n>        +\n>       -+test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success 'apply does not remove cwd incidentally' '\n>       ++\tgit diff HEAD HEAD~1 >patch &&\n>       ++\ttest_incidental_dir_removal failure git apply ../patch\n>       ++'\n>        +\n>       -+\tmkdir dirORfile &&\n>       -+\t(\n>       -+\t\tcd dirORfile &&\n>       -+\t\ttest_must_fail git cherry-pick fd_conflict 2>../error &&\n>       -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n>       -+\t) &&\n>       ++test_incidental_untracked_dir_removal () {\n>       ++\tworks=$1 &&\n>       ++\tshift &&\n>        +\n>       -+\ttest_path_is_dir dirORfile\n>       -+'\n>       ++\ttest_when_finished \"git reset --hard\" &&\n>        +\n>       -+test_expect_failure 'rebase does not remove cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++\tgit checkout foo/bar/baz^{commit} &&\n>       ++\tmkdir -p untracked &&\n>       ++\tmkdir empty\n>       ++\t>untracked/random &&\n>        +\n>        +\t(\n>       -+\t\tcd subdir &&\n>       -+\t\tgit rebase foo/bar/baz fd_conflict\n>       ++\t\tcd untracked &&\n>       ++\t\t\"$@\" &&\n>       ++\n>       ++\t\t# Although we want pwd & git status to pass, test for existing\n>       ++\t\t# rather than desired behavior.\n>       ++\t\tif [[ $works == \"success\" ]]; then\n>       ++\t\t\tpwd -P &&\n>       ++\t\t\tgit status --porcelain\n>       ++\t\telse\n>       ++\t\t\t! pwd -P &&\n>       ++\t\t\ttest_might_fail git status --porcelain\n>       ++\t\tfi\n>        +\t) &&\n>       ++\ttest_path_is_missing empty &&\n>       ++\ttest_path_is_missing untracked/random &&\n>        +\n>       -+\ttest_path_is_missing subdir/file.t &&\n>       -+\ttest_path_is_dir subdir\n>       ++\t# Although we want dir to be present, test for existing rather\n>       ++\t# than desired behavior.\n>       ++\tif [[ $works == \"success\" ]]; then\n>       ++\t\ttest_path_is_dir untracked\n>       ++\telse\n>       ++\t\ttest_path_is_missing untracked\n>       ++\tfi\n>       ++}\n>       ++\n>       ++test_expect_success 'clean does not remove cwd incidentally' '\n>       ++\ttest_incidental_untracked_dir_removal failure \\\n>       ++\t\tgit -C .. clean -fd -e warnings . >warnings\n>        +'\n>        +\n>       -+test_expect_failure 'rebase fails if cwd needs to be removed' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       -+\n>       -+\tmkdir dirORfile &&\n>       -+\t(\n>       -+\t\tcd dirORfile &&\n>       -+\t\ttest_must_fail git rebase foo/bar/baz fd_conflict 2>../error &&\n>       -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n>       -+\t) &&\n>       -+\n>       -+\ttest_path_is_dir dirORfile\n>       ++test_expect_success 'stash does not remove cwd incidentally' '\n>       ++\ttest_incidental_untracked_dir_removal failure \\\n>       ++\t\tgit stash --include-untracked\n>        +'\n>        +\n>       -+test_expect_failure 'revert does not remove cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success '`rm -rf dir` only removes a subset of dir' '\n>       ++\ttest_when_finished \"rm -rf a/\" &&\n>       ++\n>       ++\tmkdir -p a/b/c &&\n>       ++\t>a/b/c/untracked &&\n>       ++\t>a/b/c/tracked &&\n>       ++\tgit add a/b/c/tracked &&\n>        +\n>        +\t(\n>       -+\t\tcd subdir &&\n>       -+\t\tgit revert subdir/file\n>       ++\t\tcd a/b &&\n>       ++\t\tgit rm -rf ../b\n>        +\t) &&\n>        +\n>       -+\ttest_path_is_missing subdir/file.t &&\n>       -+\ttest_path_is_dir subdir\n>       ++\ttest_path_is_dir a/b &&\n>       ++\ttest_path_is_missing a/b/c/tracked &&\n>       ++\ttest_path_is_file a/b/c/untracked\n>        +'\n>        +\n>       -+test_expect_failure 'revert fails if cwd needs to be removed' '\n>       -+\tgit checkout fd_conflict &&\n>       -+\tgit revert HEAD &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n>       ++\ttest_when_finished \"rm -rf a/\" &&\n>       ++\n>       ++\tmkdir -p a/b/c &&\n>       ++\t>a/b/c/tracked &&\n>       ++\tgit add a/b/c/tracked &&\n>        +\n>       -+\tmkdir dirORfile &&\n>        +\t(\n>       -+\t\tcd dirORfile &&\n>       -+\t\ttest_must_fail git revert HEAD 2>../error &&\n>       -+\t\tgrep \"Refusing to remove the current working directory\" ../error\n>       ++\t\tcd a/b &&\n>       ++\t\tgit rm -rf ../b\n>        +\t) &&\n>        +\n>       -+\ttest_path_is_dir dirORfile\n>       ++\ttest_path_is_missing a/b/c/tracked &&\n>       ++\t## We would prefer if a/b was still present, though empty, since it\n>       ++\t## was the current working directory\n>       ++\t#test_path_is_dir a/b\n>       ++\t## But the current behavior is that it not only deletes the directory\n>       ++\t## a/b as requested, but also goes and deletes a\n>       ++\ttest_path_is_missing a\n>        +'\n>        +\n>       -+test_expect_failure 'rm does not remove cwd incidentally' '\n>       -+\ttest_when_finished \"git reset --hard\" &&\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\n>       ++test_expect_success 'git version continues working from a deleted dir' '\n>       ++\tmkdir tmp &&\n>        +\t(\n>       -+\t\tcd foo &&\n>       -+\t\tgit rm bar/baz.t\n>       -+\t) &&\n>       -+\n>       -+\ttest_path_is_missing foo/bar/baz &&\n>       -+\ttest_path_is_missing foo/bar &&\n>       -+\ttest_path_is_dir foo\n>       ++\t\tcd tmp &&\n>       ++\t\trm -rf ../tmp &&\n>       ++\t\tgit version\n>       ++\t)\n>        +'\n>        +\n>       -+test_expect_failure 'apply does not remove cwd incidentally' '\n>       -+\ttest_when_finished \"git reset --hard\" &&\n>       -+\tgit checkout foo/bar/baz &&\n>       ++test_submodule_removal () {\n>       ++\tpath_status=$1 &&\n>       ++\tshift &&\n>        +\n>       -+\t(\n>       -+\t\tcd subdir &&\n>       -+\t\tgit diff subdir/file init | git apply\n>       -+\t) &&\n>       ++\ttest_status=\n>       ++\ttest $path_status = dir && test_status=test_must_fail\n>        +\n>       -+\ttest_path_is_missing subdir/file.t &&\n>       -+\ttest_path_is_dir subdir\n>       -+'\n>       ++\t# Actually, while path_status == dir && test_status=test_must_fail\n>       ++\t# reflect our desired behavior, current behavior is:\n>       ++\tpath_status=missing\n>       ++\ttest_status=\n>       ++\n>       ++\ttest_when_finished \"git reset --hard HEAD~1\" &&\n>       ++\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n>        +\n>       -+test_expect_failure 'clean does not remove cwd incidentally' '\n>        +\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>        +\n>       -+\tmkdir empty &&\n>       -+\tmkdir untracked &&\n>       -+\t>untracked/random &&\n>       ++\tgit init my_submodule &&\n>       ++\ttouch my_submodule/file &&\n>       ++\tgit -C my_submodule add file &&\n>       ++\tgit -C my_submodule commit -m \"initial commit\" &&\n>       ++\tgit submodule add ./my_submodule &&\n>       ++\tgit commit -m \"Add the submodule\" &&\n>       ++\n>        +\t(\n>       -+\t\tcd untracked &&\n>       -+\t\tgit clean -fd -e warnings :/ >../warnings &&\n>       -+\t\tgrep \"Refusing to remove current working directory\" ../warnings\n>       ++\t\tcd my_submodule &&\n>       ++\t\t$test_status \"$@\"\n>        +\t) &&\n>        +\n>       -+\ttest_path_is_missing empty &&\n>       -+\ttest_path_is_missing untracked/random &&\n>       -+\ttest_path_is_dir untracked\n>       -+'\n>       ++\ttest_path_is_${path_status} my_submodule\n>       ++}\n>        +\n>       -+test_expect_failure 'stash does not remove cwd incidentally' '\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\ttest_when_finished \"git clean -fdx\" &&\n>       ++test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n>       ++\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n>       ++'\n>        +\n>       -+\tmkdir untracked &&\n>       -+\t>untracked/random &&\n>       -+\t(\n>       -+\t\tcd untracked &&\n>       -+\t\tgit stash --include-untracked &&\n>       -+\t\tgit status\n>       -+\t) &&\n>       ++test_expect_success 'rm -r leaves submodule if cwd inside' '\n>       ++\ttest_submodule_removal dir \\\n>       ++\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n>       ++'\n>        +\n>       -+\ttest_path_is_missing untracked/random &&\n>       -+\ttest_path_is_dir untracked\n>       ++test_expect_success 'rm -rf removes submodule even if cwd inside' '\n>       ++\ttest_submodule_removal missing \\\n>       ++\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n>        +'\n>        +\n>        +test_done\n>    2:  f6129a8ac9c !  2:  200ddece05d setup: introduce startup_info->original_cwd\n>       @@ cache.h: void overlay_tree_on_index(struct index_state *istate,\n>        +\tconst char *original_cwd;\n>         };\n>         extern struct startup_info *startup_info;\n>       ++extern const char *tmp_original_cwd;\n>         \n>       + /* merge.c */\n>       + struct commit_list;\n>        \n>       - ## git.c ##\n>       -@@ git.c: int cmd_main(int argc, const char **argv)\n>       + ## common-main.c ##\n>       +@@ common-main.c: static void restore_sigpipe_to_default(void)\n>       + int main(int argc, const char **argv)\n>       + {\n>       + \tint result;\n>       ++\tstruct strbuf tmp = STRBUF_INIT;\n>       +\n>       + \ttrace2_initialize_clock();\n>         \n>       - \ttrace_command_performance(argv);\n>       +@@ common-main.c: int main(int argc, const char **argv)\n>       + \ttrace2_cmd_start(argv);\n>       + \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n>         \n>       -+\tstartup_info->original_cwd = xgetcwd();\n>       ++\tif (!strbuf_getcwd(&tmp))\n>       ++\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n>        +\n>       - \t/*\n>       - \t * \"git-xxxx\" is the same as \"git xxxx\", but we obviously:\n>       - \t *\n>       + \tresult = cmd_main(argc, argv);\n>       +\n>       + \ttrace2_cmd_exit(result);\n>        \n>         ## setup.c ##\n>       +@@ setup.c: static int work_tree_config_is_bogus;\n>       +\n>       + static struct startup_info the_startup_info;\n>       + struct startup_info *startup_info = &the_startup_info;\n>       ++const char *tmp_original_cwd;\n>       +\n>       + /*\n>       +  * The input parameter must contain an absolute path, and it must already be\n>        @@ setup.c: void setup_work_tree(void)\n>         \tinitialized = 1;\n>         }\n>       @@ setup.c: void setup_work_tree(void)\n>        +\tconst char *worktree = NULL;\n>        +\tint offset = -1;\n>        +\n>       -+\t/*\n>       -+\t * startup_info->original_cwd wass set early on in cmd_main(), unless\n>       -+\t * we're an auxiliary tool like git-remote-http or test-tool.\n>       -+\t */\n>       -+\tif (!startup_info->original_cwd)\n>       ++\tif (!tmp_original_cwd)\n>        +\t\treturn;\n>        +\n>        +\t/*\n>        +\t * startup_info->original_cwd points to the current working\n>        +\t * directory we inherited from our parent process, which is a\n>       -+\t * directory we want to avoid incidentally removing.\n>       ++\t * directory we want to avoid removing.\n>        +\t *\n>        +\t * For convience, we would like to have the path relative to the\n>        +\t * worktree instead of an absolute path.\n>       @@ setup.c: void setup_work_tree(void)\n>        +\t */\n>        +\n>        +\t/* Normalize the directory */\n>       -+\tstrbuf_realpath(&tmp, startup_info->original_cwd, 1);\n>       -+\tfree((char*)startup_info->original_cwd);\n>       ++\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n>       ++\tfree((char*)tmp_original_cwd);\n>       ++\ttmp_original_cwd = NULL;\n>        +\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n>        +\n>       -+\t/* Find out if this is in the worktree */\n>       ++\t/*\n>       ++\t * Get our worktree; we only protect the current working directory\n>       ++\t * if it's in the worktree.\n>       ++\t */\n>        +\tworktree = get_git_work_tree();\n>       -+\tif (worktree)\n>       -+\t\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n>       ++\tif (!worktree)\n>       ++\t\tgoto no_prevention_needed;\n>       ++\n>       ++\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n>        +\tif (offset >= 0) {\n>        +\t\t/*\n>       ++\t\t * If startup_info->original_cwd == worktree, that is already\n>       ++\t\t * protected and we don't need original_cwd as a secondary\n>       ++\t\t * protection measure.\n>       ++\t\t */\n>       ++\t\tif (!*(startup_info->original_cwd + offset))\n>       ++\t\t\tgoto no_prevention_needed;\n>       ++\n>       ++\t\t/*\n>        +\t\t * original_cwd was inside worktree; precompose it just as\n>        +\t\t * we do prefix so that built up paths will match\n>        +\t\t */\n>       @@ setup.c: void setup_work_tree(void)\n>        +\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n>        +\t\t\t\t\t\t    + offset);\n>        +\t}\n>       ++\treturn;\n>       ++\n>       ++no_prevention_needed:\n>       ++\tfree((char*)startup_info->original_cwd);\n>       ++\tstartup_info->original_cwd = NULL;\n>        +}\n>        +\n>         static int read_worktree_config(const char *var, const char *value, void *vdata)\n>    3:  e74975e83cc !  3:  68ae90546fe unpack-trees: refuse to remove startup_info->original_cwd\n>       @@ Commit message\n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'checkout does not clean cwd incidentally' '\n>       - \ttest_path_is_dir foo\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success 'checkout does not clean cwd incidentally' '\n>         '\n>         \n>       --test_expect_failure 'checkout fails if cwd needs to be removed' '\n>       -+test_expect_success 'checkout fails if cwd needs to be removed' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'checkout fails if cwd needs to be removed' '\n>       +-\ttest_required_dir_removal failure git checkout fd_conflict\n>       ++\ttest_required_dir_removal success git checkout fd_conflict\n>       + '\n>       +\n>       + test_expect_success 'reset --hard does not clean cwd incidentally' '\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n>       + \t(\n>       + \t\tcd dirORfile &&\n>       +\n>       +-\t\t# We would rather this failed, but we test for existing\n>       +-\t\t# rather than desired behavior\n>       +-\t\tgit merge fd_conflict 2>../error\n>       ++\t\ttest_must_fail git merge fd_conflict 2>../error\n>       + \t) &&\n>         \n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'merge does not remove cwd incidentally' '\n>       - \ttest_path_is_dir subdir\n>       +-\t## Here is the behavior we would rather have:\n>       +-\t#test_path_is_dir dirORfile &&\n>       +-\t#grep \"Refusing to remove the current working directory\" error\n>       +-\t## But instead we test for existing behavior\n>       +-\ttest_path_is_file dirORfile &&\n>       +-\ttest_must_be_empty error\n>       ++\ttest_path_is_dir dirORfile &&\n>       ++\tgrep \"Refusing to remove the current working directory\" error\n>         '\n>         \n>       --test_expect_failure 'merge fails if cwd needs to be removed' '\n>       -+test_expect_success 'merge fails if cwd needs to be removed' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + GIT_TEST_MERGE_ALGORITHM=ort\n>         \n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n>       - \ttest_path_is_dir subdir\n>       + test_expect_success 'merge fails if cwd needs to be removed' '\n>       +-\ttest_required_dir_removal failure git merge fd_conflict\n>       ++\ttest_required_dir_removal success git merge fd_conflict\n>         '\n>         \n>       --test_expect_failure 'cherry-pick fails if cwd needs to be removed' '\n>       -+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'cherry-pick does not clean cwd incidentally' '\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick does not clean cwd incidentally' '\n>       + '\n>         \n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rebase does not remove cwd incidentally' '\n>       - \ttest_path_is_dir subdir\n>       + test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>       +-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n>       ++\ttest_required_dir_removal success git cherry-pick fd_conflict\n>         '\n>         \n>       --test_expect_failure 'rebase fails if cwd needs to be removed' '\n>       -+test_expect_success 'rebase fails if cwd needs to be removed' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'rebase does not clean cwd incidentally' '\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success 'revert does not clean cwd incidentally' '\n>       + '\n>         \n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'revert does not remove cwd incidentally' '\n>       - \ttest_path_is_dir subdir\n>       + test_expect_success 'revert fails if cwd needs to be removed' '\n>       +-\ttest_required_dir_removal failure git revert undo_fd_conflict\n>       ++\ttest_required_dir_removal success git revert undo_fd_conflict\n>         '\n>         \n>       --test_expect_failure 'revert fails if cwd needs to be removed' '\n>       -+test_expect_success 'revert fails if cwd needs to be removed' '\n>       - \tgit checkout fd_conflict &&\n>       - \tgit revert HEAD &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'rm does not clean cwd incidentally' '\n>        \n>         ## unpack-trees.c ##\n>        @@ unpack-trees.c: static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n>    4:  e06806e3a32 !  4:  1bb8905900c unpack-trees: add special cwd handling\n>       @@ Commit message\n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'reset --hard does not clean cwd incidentally' '\n>       - \ttest_path_is_dir foo\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard does not clean cwd incidentally' '\n>         '\n>         \n>       --test_expect_failure 'reset --hard fails if cwd needs to be removed' '\n>       -+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>       +-\ttest_required_dir_removal failure git reset --hard fd_conflict\n>       ++\ttest_required_dir_removal success git reset --hard fd_conflict\n>       + '\n>         \n>       + test_expect_success 'merge does not clean cwd incidentally' '\n>        \n>         ## unpack-trees.c ##\n>        @@ unpack-trees.c: static int verify_absent_1(const struct cache_entry *ce,\n>    5:  46728f74ea1 !  5:  8a33d74e7cf symlinks: do not include startup_info->original_cwd in dir removal\n>       @@ symlinks.c: void schedule_dir_for_removal(const char *name, int len)\n>         \t\t\t\t   &previous_slash);\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>       -@@ t/t2501-cwd-empty.sh: test_expect_success setup '\n>       - \tgit commit -m dirORfile\n>       - '\n>       +@@ t/t2501-cwd-empty.sh: test_required_dir_removal () {\n>       + }\n>         \n>       --test_expect_failure 'checkout does not clean cwd incidentally' '\n>       -+test_expect_success 'checkout does not clean cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_path_is_dir foo/bar &&\n>       + test_expect_success 'checkout does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git checkout init\n>       ++\ttest_incidental_dir_removal success git checkout init\n>       + '\n>         \n>       + test_expect_success 'checkout fails if cwd needs to be removed' '\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'checkout fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>         '\n>         \n>       --test_expect_failure 'reset --hard does not clean cwd incidentally' '\n>       -+test_expect_success 'reset --hard does not clean cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_path_is_dir foo/bar &&\n>       + test_expect_success 'reset --hard does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git reset --hard init\n>       ++\ttest_incidental_dir_removal success git reset --hard init\n>       + '\n>         \n>       + test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'reset --hard fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>         '\n>         \n>       --test_expect_failure 'merge does not remove cwd incidentally' '\n>       -+test_expect_success 'merge does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'merge does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git merge reverted\n>       ++\ttest_incidental_dir_removal success git merge reverted\n>       + '\n>         \n>       + # This file uses some simple merges where\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'merge fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>         '\n>         \n>       --test_expect_failure 'cherry-pick does not remove cwd incidentally' '\n>       -+test_expect_success 'cherry-pick does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       -\n>       -@@ t/t2501-cwd-empty.sh: test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>       + test_expect_success 'cherry-pick does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git cherry-pick reverted\n>       ++\ttest_incidental_dir_removal success git cherry-pick reverted\n>         '\n>         \n>       --test_expect_failure 'rebase does not remove cwd incidentally' '\n>       -+test_expect_success 'rebase does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       -\n>       + test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'rebase fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>         '\n>         \n>       --test_expect_failure 'revert does not remove cwd incidentally' '\n>       -+test_expect_success 'revert does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'revert does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git revert HEAD\n>       ++\ttest_incidental_dir_removal success git revert HEAD\n>       + '\n>         \n>       + test_expect_success 'revert fails if cwd needs to be removed' '\n>    6:  01ce9444dae !  6:  11e4ec881bb clean: do not attempt to remove startup_info->original_cwd\n>       @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n>        +\t\tstrbuf_realpath(&realpath, path->buf, 1);\n>        +\n>        +\t\t/*\n>       -+\t\t * path and realpath are absolute; for comparison, we want\n>       -+\t\t * startup_info->original_cwd to be an absolute path too.  We\n>       -+\t\t * can use strbuf_realpath for this.  Also, if original_cwd\n>       -+\t\t * started out as the empty string, then it corresponded to\n>       -+\t\t * the top of the worktree, which is protected by other means\n>       -+\t\t * so we just leave it blank.\n>       ++\t\t * path and realpath are absolute; for comparison, we would\n>       ++\t\t * like to transform startup_info->original_cwd to an absolute\n>       ++\t\t * path too.\n>        +\t\t */\n>       -+\t\t if (*startup_info->original_cwd)\n>       ++\t\t if (startup_info->original_cwd)\n>        +\t\t\t strbuf_realpath(&real_ocwd,\n>        +\t\t\t\t\t startup_info->original_cwd, 1);\n>        +\n>       @@ builtin/clean.c: static int remove_dirs(struct strbuf *path, const char *prefix,\n>         \treturn ret;\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'apply does not remove cwd incidentally' '\n>       - \ttest_path_is_dir subdir\n>       - '\n>       +@@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n>       + }\n>         \n>       --test_expect_failure 'clean does not remove cwd incidentally' '\n>       -+test_expect_success 'clean does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'clean does not remove cwd incidentally' '\n>       +-\ttest_incidental_untracked_dir_removal failure \\\n>       +-\t\tgit -C .. clean -fd -e warnings . >warnings\n>       ++\ttest_incidental_untracked_dir_removal success \\\n>       ++\t\tgit -C .. clean -fd -e warnings . >warnings &&\n>       ++\tgrep \"Refusing to remove current working directory\" warnings\n>       + '\n>         \n>       + test_expect_success 'stash does not remove cwd incidentally' '\n>    -:  ----------- >  7:  39b1f3a225e rebase: do not attempt to remove startup_info->original_cwd\n>    7:  edec0894ca2 !  8:  0110462a19c stash: do not attempt to remove startup_info->original_cwd\n>       @@ Metadata\n>         ## Commit message ##\n>            stash: do not attempt to remove startup_info->original_cwd\n>        \n>       +    Since stash spawns a `clean` subprocess, make sure we run that from the\n>       +    startup_info->original_cwd directory, so that the `clean` processs knows\n>       +    to protect that directory.  Also, since the `clean` command might no\n>       +    longer run from the toplevel, pass the ':/' magic pathspec to ensure we\n>       +    still clean from the toplevel.\n>       +\n>            Signed-off-by: Elijah Newren <newren@gmail.com>\n>        \n>         ## builtin/stash.c ##\n>       @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char\n>         \n>         \t\t\tcp.git_cmd = 1;\n>        +\t\t\tif (startup_info->original_cwd &&\n>       -+\t\t\t    *startup_info->original_cwd &&\n>        +\t\t\t    !is_absolute_path(startup_info->original_cwd))\n>        +\t\t\t\tcp.dir = startup_info->original_cwd;\n>         \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n>       @@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'clean does not remove cwd incidentally' '\n>       - \ttest_path_is_dir untracked\n>         '\n>         \n>       --test_expect_failure 'stash does not remove cwd incidentally' '\n>       -+test_expect_success 'stash does not remove cwd incidentally' '\n>       - \tgit checkout foo/bar/baz &&\n>       - \ttest_when_finished \"git clean -fdx\" &&\n>       + test_expect_success 'stash does not remove cwd incidentally' '\n>       +-\ttest_incidental_untracked_dir_removal failure \\\n>       ++\ttest_incidental_untracked_dir_removal success \\\n>       + \t\tgit stash --include-untracked\n>       + '\n>         \n>    8:  1815f18592b !  9:  2c73a09a2e8 dir: avoid incidentally removing the original_cwd in remove_path()\n>       @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>        @@ t/t2501-cwd-empty.sh: test_expect_success 'revert fails if cwd needs to be removed' '\n>       - \ttest_path_is_dir dirORfile\n>         '\n>         \n>       --test_expect_failure 'rm does not remove cwd incidentally' '\n>       -+test_expect_success 'rm does not remove cwd incidentally' '\n>       - \ttest_when_finished \"git reset --hard\" &&\n>       - \tgit checkout foo/bar/baz &&\n>       + test_expect_success 'rm does not clean cwd incidentally' '\n>       +-\ttest_incidental_dir_removal failure git rm bar/baz.t\n>       ++\ttest_incidental_dir_removal success git rm bar/baz.t\n>       + '\n>         \n>       -@@ t/t2501-cwd-empty.sh: test_expect_failure 'rm does not remove cwd incidentally' '\n>       - \ttest_path_is_dir foo\n>       + test_expect_success 'apply does not remove cwd incidentally' '\n>       + \tgit diff HEAD HEAD~1 >patch &&\n>       +-\ttest_incidental_dir_removal failure git apply ../patch\n>       ++\ttest_incidental_dir_removal success git apply ../patch\n>         '\n>         \n>       --test_expect_failure 'apply does not remove cwd incidentally' '\n>       -+test_expect_success 'apply does not remove cwd incidentally' '\n>       - \ttest_when_finished \"git reset --hard\" &&\n>       - \tgit checkout foo/bar/baz &&\n>       + test_incidental_untracked_dir_removal () {\n>       +@@ t/t2501-cwd-empty.sh: test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n>       + \t) &&\n>       +\n>       + \ttest_path_is_missing a/b/c/tracked &&\n>       +-\t## We would prefer if a/b was still present, though empty, since it\n>       +-\t## was the current working directory\n>       +-\t#test_path_is_dir a/b\n>       +-\t## But the current behavior is that it not only deletes the directory\n>       +-\t## a/b as requested, but also goes and deletes a\n>       +-\ttest_path_is_missing a\n>       ++\ttest_path_is_missing a/b/c &&\n>       ++\ttest_path_is_dir a/b\n>       + '\n>         \n>       + test_expect_success 'git version continues working from a deleted dir' '\n>    9:  adaad7aeaac ! 10:  d4e50b4053d dir: new flag to remove_dir_recurse() to spare the original_cwd\n>       @@ dir.h: int get_sparse_checkout_patterns(struct pattern_list *pl);\n>          * of the above REMOVE_DIR_* constants. Return 0 on success.\n>        \n>         ## t/t2501-cwd-empty.sh ##\n>       -@@ t/t2501-cwd-empty.sh: test_expect_success 'stash does not remove cwd incidentally' '\n>       - \ttest_path_is_dir untracked\n>       - '\n>       +@@ t/t2501-cwd-empty.sh: test_submodule_removal () {\n>       + \ttest_status=\n>       + \ttest $path_status = dir && test_status=test_must_fail\n>       +\n>       +-\t# Actually, while path_status == dir && test_status=test_must_fail\n>       +-\t# reflect our desired behavior, current behavior is:\n>       +-\tpath_status=missing\n>       +-\ttest_status=\n>       +-\n>       + \ttest_when_finished \"git reset --hard HEAD~1\" &&\n>       + \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n>         \n>       -+test_expect_success 'rm -r leaves submodule if cwd inside' '\n>       -+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n>       -+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n>       -+\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\n>       -+\tgit init my_submodule &&\n>       -+\ttouch my_submodule/file &&\n>       -+\tgit -C my_submodule add file &&\n>       -+\tgit -C my_submodule commit -m \"initial commit\" &&\n>       -+\tgit submodule add ./my_submodule &&\n>       -+\tgit commit -m \"Add the submodule\" &&\n>       -+\n>       -+\t(\n>       -+\t\tcd my_submodule &&\n>       -+\t\ttest_must_fail git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n>       -+\t) &&\n>       -+\n>       -+\ttest_path_is_dir my_submodule\n>       -+'\n>       -+\n>       -+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n>       -+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n>       -+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n>       -+\n>       -+\tgit checkout foo/bar/baz &&\n>       -+\n>       -+\tgit init my_submodule &&\n>       -+\ttouch my_submodule/file &&\n>       -+\tgit -C my_submodule add file &&\n>       -+\tgit -C my_submodule commit -m \"initial commit\" &&\n>       -+\tgit submodule add ./my_submodule &&\n>       -+\tgit commit -m \"Add the submodule\" &&\n>       -+\n>       -+\t(\n>       -+\t\tcd my_submodule &&\n>       -+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n>       -+\t) &&\n>       -+\n>       -+\ttest_path_is_missing my_submodule\n>       -+'\n>       -+\n>       - test_done\n>    -:  ----------- > 11:  7eb6281be4b t2501: simplify the tests since we can now assume desired behavior\n> \n\n"},{"id":"442723","messageId":"CABPp-BHpdhfw7SQZfVKcQoPvwwjiN+3aaM_XNJS0bWottr73jg@mail.gmail.com","threadId":"56952","inReplyTo":"8e8b95d7-a4b4-6839-c572-8a43bbae8e0c@gmail.com","subject":"Re: [PATCH v3 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-01T00:03:38Z","receivedAt":"2021-12-01T00:04:45Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Hi Phillip,\n\nOn Tue, Nov 30, 2021 at 3:04 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> Hi Elijah\n>\n> On 26/11/2021 22:40, Elijah Newren via GitGitGadget wrote:\n> > Traditionally, if folks run git commands such as checkout or rebase from a\n> > subdirectory, that git command could remove their current working directory\n> > and result in subsequent git and non-git commands either getting confused or\n> > printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> > working directory: No such file or directory\"). Many commands either\n> > silently avoid removing directories that are not empty (i.e. those that have\n> > untracked or modified files in them)[1], or show an error and abort,\n> > depending on which is more appropriate for the command in question. With\n> > this series, we augment the reasons to avoid removing directories to include\n> > not just has-untracked-or-modified-files, but also to avoid removing the\n> > original_cwd as well.\n> >\n> > Peff and Junio provided some good pros/cons, if it helps:\n> >\n> >   * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n> >   * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n> >\n> > [1] well, with a few exceptions; see\n> > https://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n> > [2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n> > [3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\n> > https://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n>\n>\n> Thanks for working on this, I'm sorry I haven't had time to take a\n> proper look at it but I think it is a good idea. I did notice that\n> you're using \":/\" in patch 8 and wondered what happens if some runs 'git\n> --literal-pathspecs stash' but I haven't looked properly.\n\nInteresting point.  The --literal-pathspecs passed to stash is not\npassed along to the git clean subprocess, and we only use the \":/\"\npathspec with this subprocess.  This means whereas before we'd always\nrun\n    git clean --force --quiet -d [-x]\nfrom the toplevel, now we run\n    git clean --force --quiet -d :/ [-x]\nfrom the relevant subdirectory.  So, I don't think I've introduced any bug here.\n\n(Now, the fact that we use a git clean subprocess instead of just\ncalling the appropriate library functions is IMO a\ndesign/implementation bug, but a longstanding one.)\n"},{"id":"442744","messageId":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v4.git.git.1638225434.gitgitgadget@gmail.com","subject":"[PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:43Z","receivedAt":"2021-12-01T06:40:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). Many commands either\nsilently avoid removing directories that are not empty (i.e. those that have\nuntracked or modified files in them)[1], or show an error and abort,\ndepending on which is more appropriate for the command in question. With\nthis series, we augment the reasons to avoid removing directories to include\nnot just has-untracked-or-modified-files, but also to avoid removing the\noriginal_cwd as well.\n\nPeff and Junio provided some good pros/cons, if it helps:\n\n * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\nhttps://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\nChanges since v4:\n\n * actually fix bashism\n\nChanges since v3:\n\n * fixed one codepath from v2 so that the series really is only about the\n   working tree\n * used test-tool getcwd instead of pwd -P as suggested by Ævar for some\n   less common platforms\n * fixed bashism\n * check for clean index/worktree after verifying that expected-to-abort\n   codepaths do abort, to make it clearer that we expect an early abort\n * remove a leftover (and confusing) is_absolute_dir() check in sequencer\n   and stash from an earlier round of the series\n\nChanges since v2:\n\n * the series is now only about the working tree. So if the original cwd is\n   outside the worktree (or we're in a bare repo), then the new code is a\n   no-op.\n * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n   xgetcwd())\n * modified the initial tests to show both expected and desired behavior.\n   subsequent patches fix the tests. One new patch added at the end which\n   simplifies the tests to only check for desired behavior.\n * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n   that is already protected and we don't need secondary protection for it.\n   This simplified some other codepaths so we don't have to check for\n   startup_info->original_cwd == \"\".\n * clarified some commit messages\n\nChanges since v1:\n\n * clarified multiple commit messages\n * renamed the_cwd to startup_info->original_cwd to make it clearer that\n   it's our parent process'es cwd that really matters, which we inherited at\n   program startup. Also pulls it out of the global namespace.\n * Normalize the path for startup_info->original_cwd, and ensure that it's\n   actually the original cwd even if -C is passed to git.\n * small code cleanups suggested by René and Ævar\n * split the final patch (which got the most comments) into two -- one for\n   each function being modified. Also, add a bunch more history to the first\n   of the two resulting commit messages\n * no longer has a content conflict with so/stash-staged\n * add another value for the flags parameter that remove_dir_recursively()\n   takes so that it can opt into either the old or the new behavior. Use\n   that for the one special corner case I could find where it matters, and\n   add a few tests around it to highlight the utility of the flag.\n\nElijah Newren (11):\n  t2501: add various tests for removing the current working directory\n  setup: introduce startup_info->original_cwd\n  unpack-trees: refuse to remove startup_info->original_cwd\n  unpack-trees: add special cwd handling\n  symlinks: do not include startup_info->original_cwd in dir removal\n  clean: do not attempt to remove startup_info->original_cwd\n  rebase: do not attempt to remove startup_info->original_cwd\n  stash: do not attempt to remove startup_info->original_cwd\n  dir: avoid incidentally removing the original_cwd in remove_path()\n  dir: new flag to remove_dir_recurse() to spare the original_cwd\n  t2501: simplify the tests since we can now assume desired behavior\n\n builtin/clean.c      |  44 +++++--\n builtin/rm.c         |   3 +-\n builtin/stash.c      |   4 +-\n cache.h              |   2 +\n common-main.c        |   4 +\n dir.c                |  15 ++-\n dir.h                |   9 +-\n sequencer.c          |   2 +\n setup.c              |  65 ++++++++++\n symlinks.c           |   8 +-\n t/t2501-cwd-empty.sh | 277 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  30 ++++-\n unpack-trees.h       |   1 +\n 13 files changed, 442 insertions(+), 22 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v5\nPull-Request: https://github.com/git/git/pull/1140\n\nRange-diff vs v4:\n\n  1:  a45b3f08802 !  1:  7c72e888d2e t2501: add various tests for removing the current working directory\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t\t# Although we want pwd & git status to pass, test for existing\n      +\t\t# rather than desired behavior.\n     -+\t\tif test \"$works\" == \"success\"\n     ++\t\tif test \"$works\" = \"success\"\n      +\t\tthen\n      +\t\t\ttest-tool getcwd &&\n      +\t\t\tgit status --porcelain\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t# Although we want dir to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif test \"$works\" == \"success\"\n     ++\tif test \"$works\" = \"success\"\n      +\tthen\n      +\t\ttest_path_is_dir foo\n      +\telse\n     @@ t/t2501-cwd-empty.sh (new)\n      +\t\t# was an untracked file there), and for the index and worktree\n      +\t\t# to be left clean with pwd and git status working afterwards.\n      +\t\t# But test for existing rather than desired behavior.\n     -+\t\tif test \"$works\" == \"success\"\n     ++\t\tif test \"$works\" = \"success\"\n      +\t\tthen\n      +\t\t\ttest_must_fail \"$@\" 2>../error &&\n      +\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t# Although we want dirORfile to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif test \"$works\" == \"success\"\n     ++\tif test \"$works\" = \"success\"\n      +\tthen\n      +\t\ttest_path_is_dir dirORfile\n      +\telse\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t\t# Although we want pwd & git status to pass, test for existing\n      +\t\t# rather than desired behavior.\n     -+\t\tif test \"$works\" == \"success\"\n     ++\t\tif test \"$works\" = \"success\"\n      +\t\tthen\n      +\t\t\ttest-tool getcwd &&\n      +\t\t\tgit status --porcelain\n     @@ t/t2501-cwd-empty.sh (new)\n      +\n      +\t# Although we want dir to be present, test for existing rather\n      +\t# than desired behavior.\n     -+\tif test \"$works\" == \"success\"\n     ++\tif test \"$works\" = \"success\"\n      +\tthen\n      +\t\ttest_path_is_dir untracked\n      +\telse\n     @@ t/t2501-cwd-empty.sh (new)\n      +\ttest_status=\n      +\ttest \"$path_status\" = dir && test_status=test_must_fail\n      +\n     -+\t# Actually, while path_status == dir && test_status=test_must_fail\n     ++\t# Actually, while path_status=dir && test_status=test_must_fail\n      +\t# reflect our desired behavior, current behavior is:\n      +\tpath_status=missing\n      +\ttest_status=\n  2:  ca9f632bd11 =  2:  37f333b2024 setup: introduce startup_info->original_cwd\n  3:  41a82eff41e =  3:  b611c73bd15 unpack-trees: refuse to remove startup_info->original_cwd\n  4:  2e2ea02f97b =  4:  706415a4547 unpack-trees: add special cwd handling\n  5:  f444a541da4 =  5:  66ef6b4d943 symlinks: do not include startup_info->original_cwd in dir removal\n  6:  1990e36bb41 =  6:  54d059c683e clean: do not attempt to remove startup_info->original_cwd\n  7:  1035ee7f9ce =  7:  f8efb7446c3 rebase: do not attempt to remove startup_info->original_cwd\n  8:  a2be40a22d1 =  8:  2855ed45112 stash: do not attempt to remove startup_info->original_cwd\n  9:  834031be9e0 =  9:  23dfc3e399d dir: avoid incidentally removing the original_cwd in remove_path()\n 10:  d5750fcb6d5 ! 10:  fe47c0f0c17 dir: new flag to remove_dir_recurse() to spare the original_cwd\n     @@ t/t2501-cwd-empty.sh: test_submodule_removal () {\n       \ttest_status=\n       \ttest \"$path_status\" = dir && test_status=test_must_fail\n       \n     --\t# Actually, while path_status == dir && test_status=test_must_fail\n     +-\t# Actually, while path_status=dir && test_status=test_must_fail\n      -\t# reflect our desired behavior, current behavior is:\n      -\tpath_status=missing\n      -\ttest_status=\n 11:  21ff99a767c ! 11:  431dd651a7e t2501: simplify the tests since we can now assume desired behavior\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \n      -\t\t# Although we want pwd & git status to pass, test for existing\n      -\t\t# rather than desired behavior.\n     --\t\tif test \"$works\" == \"success\"\n     +-\t\tif test \"$works\" = \"success\"\n      -\t\tthen\n      -\t\t\ttest-tool getcwd &&\n      -\t\t\tgit status --porcelain\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \n      -\t# Although we want dir to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif test \"$works\" == \"success\"\n     +-\tif test \"$works\" = \"success\"\n      -\tthen\n      -\t\ttest_path_is_dir foo\n      -\telse\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n      -\t\t# was an untracked file there), and for the index and worktree\n      -\t\t# to be left clean with pwd and git status working afterwards.\n      -\t\t# But test for existing rather than desired behavior.\n     --\t\tif test \"$works\" == \"success\"\n     +-\t\tif test \"$works\" = \"success\"\n      -\t\tthen\n      -\t\t\ttest_must_fail \"$@\" 2>../error &&\n      -\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n     @@ t/t2501-cwd-empty.sh: test_incidental_dir_removal () {\n       \n      -\t# Although we want dirORfile to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif test \"$works\" == \"success\"\n     +-\tif test \"$works\" = \"success\"\n      -\tthen\n      -\t\ttest_path_is_dir dirORfile\n      -\telse\n     @@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n       \n      -\t\t# Although we want pwd & git status to pass, test for existing\n      -\t\t# rather than desired behavior.\n     --\t\tif test \"$works\" == \"success\"\n     +-\t\tif test \"$works\" = \"success\"\n      -\t\tthen\n      -\t\t\ttest-tool getcwd &&\n      -\t\t\tgit status --porcelain\n     @@ t/t2501-cwd-empty.sh: test_incidental_untracked_dir_removal () {\n       \n      -\t# Although we want dir to be present, test for existing rather\n      -\t# than desired behavior.\n     --\tif test \"$works\" == \"success\"\n     +-\tif test \"$works\" = \"success\"\n      -\tthen\n      -\t\ttest_path_is_dir untracked\n      -\telse\n\n-- \ngitgitgadget\n"},{"id":"442745","messageId":"7c72e888d2e4434103f99e067e8880e44d99687e.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:44Z","receivedAt":"2021-12-01T06:41:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove directories left empty as a \"convenience\"\nafter removing files within them.  That is normally fine, but removing\nthe current working directory can be rather inconvenient since it can\ncause confusion for the user when they run subsequent commands.  For\nexample, after one git process has removed the current working\ndirectory, git status/log/diff will all abort with the message:\n\n    fatal: Unable to read current working directory: No such file or directory\n\nWe also have code paths that, when a file needs to be placed where a\ndirectory is (due to e.g. checkout, merge, reset, whatever), will check\nif this is okay and error out if not.  These rules include:\n  * all tracked files under that directory are intended to be removed by\n    the operation\n  * none of the tracked files under that directory have uncommitted\n    modification\n  * there are no untracked files under that directory\nHowever, if we end up remove the current working directory, we can cause\nuser confusion when they run subsequent commands, so we would prefer if\nthere was a fourth rule added to this list: avoid removing the current\nworking directory.\n\nSince there are several code paths that can result in the current\nworking directory being removed, add several tests of various different\ncodepaths.  To make it clearer what the difference between the current\nbehavior and the behavior at the end of the series, code both of them\ninto the tests and have the appropriate behavior be selected by a flag.\nSubsequent commits will toggle the flag from current to desired\nbehavior.\n\nAlso add a few tests suggested during the review of earlier rounds of\nthis patch series.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 342 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..a05abd18187\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,342 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tmkdir dirORfile &&\n+\ttest_commit dirORfile/foo &&\n+\n+\tgit rm -r dirORfile &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile &&\n+\n+\tgit switch -c df_conflict HEAD~1 &&\n+\ttest_commit random_file &&\n+\n+\tgit switch -c undo_fd_conflict fd_conflict &&\n+\tgit revert HEAD\n+'\n+\n+test_incidental_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir foo\n+\telse\n+\t\ttest_path_is_missing foo\n+\tfi\n+}\n+\n+test_required_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\tgit checkout df_conflict^{commit} &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We'd like for the command to fail (much as it would if there\n+\t\t# was an untracked file there), and for the index and worktree\n+\t\t# to be left clean with pwd and git status working afterwards.\n+\t\t# But test for existing rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t\tgit diff --exit-code HEAD &&\n+\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t\"$@\" &&\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\n+\t# Although we want dirORfile to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir dirORfile\n+\telse\n+\t\ttest_path_is_file dirORfile\n+\tfi\n+}\n+\n+test_expect_success 'checkout does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git checkout init\n+'\n+\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git checkout fd_conflict\n+'\n+\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git reset --hard init\n+'\n+\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git reset --hard fd_conflict\n+'\n+\n+test_expect_success 'merge does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git merge reverted\n+'\n+\n+# This file uses some simple merges where\n+#   Base: 'dirORfile/' exists\n+#   Side1: random other file changed\n+#   Side2: 'dirORfile/' removed, 'dirORfile' added\n+# this should resolve cleanly, but merge-recursive throws merge conflicts\n+# because it's dumb.  Add a special test for checking merge-recursive (and\n+# merge-ort), then after this just hard require ort for all remaining tests.\n+#\n+test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We would rather this failed, but we test for existing\n+\t\t# rather than desired behavior\n+\t\tgit merge fd_conflict 2>../error\n+\t) &&\n+\n+\t## Here is the behavior we would rather have:\n+\t#test_path_is_dir dirORfile &&\n+\t#grep \"Refusing to remove the current working directory\" error\n+\t## But instead we test for existing behavior\n+\ttest_path_is_file dirORfile &&\n+\ttest_must_be_empty error\n+'\n+\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n+test_expect_success 'merge fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git merge fd_conflict\n+'\n+\n+test_expect_success 'cherry-pick does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git cherry-pick reverted\n+'\n+\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+'\n+\n+test_expect_success 'rebase does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rebase reverted\n+'\n+\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git rebase fd_conflict\n+'\n+\n+test_expect_success 'revert does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git revert HEAD\n+'\n+\n+test_expect_success 'revert fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git revert undo_fd_conflict\n+'\n+\n+test_expect_success 'rm does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rm bar/baz.t\n+'\n+\n+test_expect_success 'apply does not remove cwd incidentally' '\n+\tgit diff HEAD HEAD~1 >patch &&\n+\ttest_incidental_dir_removal failure git apply ../patch\n+'\n+\n+test_incidental_untracked_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\tmkdir -p untracked &&\n+\tmkdir empty\n+\t>untracked/random &&\n+\n+\t(\n+\t\tcd untracked &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir untracked\n+\telse\n+\t\ttest_path_is_missing untracked\n+\tfi\n+}\n+\n+test_expect_success 'clean does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings\n+'\n+\n+test_expect_success 'stash does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit stash --include-untracked\n+'\n+\n+test_expect_success '`rm -rf dir` only removes a subset of dir' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/untracked &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_dir a/b &&\n+\ttest_path_is_missing a/b/c/tracked &&\n+\ttest_path_is_file a/b/c/untracked\n+'\n+\n+test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_missing a/b/c/tracked &&\n+\t## We would prefer if a/b was still present, though empty, since it\n+\t## was the current working directory\n+\t#test_path_is_dir a/b\n+\t## But the current behavior is that it not only deletes the directory\n+\t## a/b as requested, but also goes and deletes a\n+\ttest_path_is_missing a\n+'\n+\n+test_expect_success 'git version continues working from a deleted dir' '\n+\tmkdir tmp &&\n+\t(\n+\t\tcd tmp &&\n+\t\trm -rf ../tmp &&\n+\t\tgit version\n+\t)\n+'\n+\n+test_submodule_removal () {\n+\tpath_status=$1 &&\n+\tshift &&\n+\n+\ttest_status=\n+\ttest \"$path_status\" = dir && test_status=test_must_fail\n+\n+\t# Actually, while path_status=dir && test_status=test_must_fail\n+\t# reflect our desired behavior, current behavior is:\n+\tpath_status=missing\n+\ttest_status=\n+\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\t$test_status \"$@\"\n+\t) &&\n+\n+\ttest_path_is_${path_status} my_submodule\n+}\n+\n+test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n+'\n+\n+test_expect_success 'rm -r leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n+'\n+\n+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n+\ttest_submodule_removal missing \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"442746","messageId":"37f333b20240e8da62f148945387044cefb39c14.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:45Z","receivedAt":"2021-12-01T06:41:03Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands run from that directory to get confused and fail with a message\nabout being unable to read the current working directory:\n\n    $ git status\n    fatal: Unable to read current working directory: No such file or directory\n\nNon-git commands likely have similar warnings or even errors, e.g.\n\n    $ bash -c 'echo hello'\n    shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n    hello\n\nThis confuses end users, particularly since the command they get the\nerror from is not the one that caused the problem; the problem came from\nthe side-effect of some previous command.\n\nWe would like to avoid removing the current working directory of our\nparent process; towards this end, introduce a new variable,\nstartup_info->original_cwd, that tracks the current working directory\nthat we inherited from our parent process.  For convenience of later\ncomparisons, we prefer that this new variable store a path relative to\nthe toplevel working directory (thus much like 'prefix'), except without\nthe trailing slash.\n\nSubsequent commits will make use of this new variable.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n cache.h       |  2 ++\n common-main.c |  4 ++++\n setup.c       | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex eba12487b99..92e181ea759 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1834,8 +1834,10 @@ void overlay_tree_on_index(struct index_state *istate,\n struct startup_info {\n \tint have_repository;\n \tconst char *prefix;\n+\tconst char *original_cwd;\n };\n extern struct startup_info *startup_info;\n+extern const char *tmp_original_cwd;\n \n /* merge.c */\n struct commit_list;\ndiff --git a/common-main.c b/common-main.c\nindex 71e21dd20a3..aa8d5aba5bb 100644\n--- a/common-main.c\n+++ b/common-main.c\n@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)\n int main(int argc, const char **argv)\n {\n \tint result;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \n \ttrace2_initialize_clock();\n \n@@ -49,6 +50,9 @@ int main(int argc, const char **argv)\n \ttrace2_cmd_start(argv);\n \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n \n+\tif (!strbuf_getcwd(&tmp))\n+\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n+\n \tresult = cmd_main(argc, argv);\n \n \ttrace2_cmd_exit(result);\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..af3b8c09abe 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -12,6 +12,7 @@ static int work_tree_config_is_bogus;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n+const char *tmp_original_cwd;\n \n /*\n  * The input parameter must contain an absolute path, and it must already be\n@@ -432,6 +433,69 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static void setup_original_cwd(void)\n+{\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tconst char *worktree = NULL;\n+\tint offset = -1;\n+\n+\tif (!tmp_original_cwd)\n+\t\treturn;\n+\n+\t/*\n+\t * startup_info->original_cwd points to the current working\n+\t * directory we inherited from our parent process, which is a\n+\t * directory we want to avoid removing.\n+\t *\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n+\t *\n+\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+\t * but differs in two ways:\n+\t *   - prefix has a trailing '/'\n+\t *   - if the user passes '-C' to git, that modifies the prefix but\n+\t *     not startup_info->original_cwd.\n+\t */\n+\n+\t/* Normalize the directory */\n+\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n+\tfree((char*)tmp_original_cwd);\n+\ttmp_original_cwd = NULL;\n+\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n+\n+\t/*\n+\t * Get our worktree; we only protect the current working directory\n+\t * if it's in the worktree.\n+\t */\n+\tworktree = get_git_work_tree();\n+\tif (!worktree)\n+\t\tgoto no_prevention_needed;\n+\n+\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n+\tif (offset >= 0) {\n+\t\t/*\n+\t\t * If startup_info->original_cwd == worktree, that is already\n+\t\t * protected and we don't need original_cwd as a secondary\n+\t\t * protection measure.\n+\t\t */\n+\t\tif (!*(startup_info->original_cwd + offset))\n+\t\t\tgoto no_prevention_needed;\n+\n+\t\t/*\n+\t\t * original_cwd was inside worktree; precompose it just as\n+\t\t * we do prefix so that built up paths will match\n+\t\t */\n+\t\tstartup_info->original_cwd = \\\n+\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n+\t\t\t\t\t\t    + offset);\n+\t\treturn;\n+\t}\n+\n+no_prevention_needed:\n+\tfree((char*)startup_info->original_cwd);\n+\tstartup_info->original_cwd = NULL;\n+}\n+\n static int read_worktree_config(const char *var, const char *value, void *vdata)\n {\n \tstruct repository_format *data = vdata;\n@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n+\tsetup_original_cwd();\n \n \tstrbuf_release(&dir);\n \tstrbuf_release(&gitdir);\n-- \ngitgitgadget\n\n"},{"id":"442747","messageId":"b611c73bd158b31d200d324cfc3b141161ac0b96.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 03/11] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:46Z","receivedAt":"2021-12-01T06:41:04Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory we inherited\nfrom our parent process.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 20 +++++++-------------\n unpack-trees.c       | 17 +++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 21 insertions(+), 17 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex a05abd18187..398908dfc93 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -113,7 +113,7 @@ test_expect_success 'checkout does not clean cwd incidentally' '\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git checkout fd_conflict\n+\ttest_required_dir_removal success git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n@@ -144,23 +144,17 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We would rather this failed, but we test for existing\n-\t\t# rather than desired behavior\n-\t\tgit merge fd_conflict 2>../error\n+\t\ttest_must_fail git merge fd_conflict 2>../error\n \t) &&\n \n-\t## Here is the behavior we would rather have:\n-\t#test_path_is_dir dirORfile &&\n-\t#grep \"Refusing to remove the current working directory\" error\n-\t## But instead we test for existing behavior\n-\ttest_path_is_file dirORfile &&\n-\ttest_must_be_empty error\n+\ttest_path_is_dir dirORfile &&\n+\tgrep \"Refusing to remove the current working directory\" error\n '\n \n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git merge fd_conflict\n+\ttest_required_dir_removal success git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n@@ -168,7 +162,7 @@ test_expect_success 'cherry-pick does not clean cwd incidentally' '\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+\ttest_required_dir_removal success git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n@@ -184,7 +178,7 @@ test_expect_success 'revert does not clean cwd incidentally' '\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git revert undo_fd_conflict\n+\ttest_required_dir_removal success git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..6bc16f3a714 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose startup_info->original_cwd */\n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"442748","messageId":"706415a454722fdb4e002104d5929c82d9c6eedb.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 04/11] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:47Z","receivedAt":"2021-12-01T06:41:07Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the_original_cwd\nbeing in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\nthere is no untracked checking to be done, so we simply add a special\ncase near the top of verify_absent_1.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 13 +++++++++++--\n 2 files changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 398908dfc93..5af1fec6fec 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -121,7 +121,7 @@ test_expect_success 'reset --hard does not clean cwd incidentally' '\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git reset --hard fd_conflict\n+\ttest_required_dir_removal success git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 6bc16f3a714..5852807d2fb 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking startup_info->original_cwd... */\n+\t\tif (startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"442749","messageId":"66ef6b4d9432d3725b48c19179bbf3095092e00e.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 05/11] symlinks: do not include startup_info->original_cwd in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:48Z","receivedAt":"2021-12-01T06:41:08Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude startup_info->original_cwd and leave it around.  This\navoids the user getting confused by subsequent git commands (and non-git\ncommands) that would otherwise report confusing messages about being\nunable to read the current working directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           |  8 +++++++-\n t/t2501-cwd-empty.sh | 10 +++++-----\n 2 files changed, 12 insertions(+), 6 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..c667baa949b 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif ((startup_info->original_cwd &&\n+\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n+\t\t    rmdir(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(name, startup_info->original_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 5af1fec6fec..e4502d24d57 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -109,7 +109,7 @@ test_required_dir_removal () {\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git checkout init\n+\ttest_incidental_dir_removal success git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n@@ -117,7 +117,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git reset --hard init\n+\ttest_incidental_dir_removal success git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n@@ -125,7 +125,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git merge reverted\n+\ttest_incidental_dir_removal success git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -158,7 +158,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git cherry-pick reverted\n+\ttest_incidental_dir_removal success git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n@@ -174,7 +174,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git revert HEAD\n+\ttest_incidental_dir_removal success git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-- \ngitgitgadget\n\n"},{"id":"442750","messageId":"54d059c683ef3639db1acac51e26c9328c9684c5.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 06/11] clean: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:49Z","receivedAt":"2021-12-01T06:41:09Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 44 +++++++++++++++++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  5 +++--\n 2 files changed, 38 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..3ff02bbbffe 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n {\n \tDIR *dir;\n \tstruct strbuf quoted = STRBUF_INIT;\n+\tstruct strbuf realpath = STRBUF_INIT;\n+\tstruct strbuf real_ocwd = STRBUF_INIT;\n \tstruct dirent *e;\n \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n \tstruct string_list dels = STRING_LIST_INIT_DUP;\n@@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t/*\n+\t\t * Normalize path components in path->buf, e.g. change '\\' to\n+\t\t * '/' on Windows.\n+\t\t */\n+\t\tstrbuf_realpath(&realpath, path->buf, 1);\n+\n+\t\t/*\n+\t\t * path and realpath are absolute; for comparison, we would\n+\t\t * like to transform startup_info->original_cwd to an absolute\n+\t\t * path too.\n+\t\t */\n+\t\t if (startup_info->original_cwd)\n+\t\t\t strbuf_realpath(&real_ocwd,\n+\t\t\t\t\t startup_info->original_cwd, 1);\n+\n+\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n \t}\n out:\n+\tstrbuf_release(&realpath);\n+\tstrbuf_release(&real_ocwd);\n \tstrbuf_release(&quoted);\n \tstring_list_clear(&dels, 0);\n \treturn ret;\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex e4502d24d57..b1182390ba3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -230,8 +230,9 @@ test_incidental_untracked_dir_removal () {\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n-\t\tgit -C .. clean -fd -e warnings . >warnings\n+\ttest_incidental_untracked_dir_removal success \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings &&\n+\tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442751","messageId":"f8efb7446c33f14631b088ac043aca8a403a3250.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:50Z","receivedAt":"2021-12-01T06:41:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince rebase spawns a `checkout` subprocess, make sure we run that from\nthe startup_info->original_cwd directory, so that the checkout process\nknows to protect that directory.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n sequencer.c          | 2 ++\n t/t2501-cwd-empty.sh | 4 ++--\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex ea96837cde3..83f257e7fa4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n \n \tcmd.git_cmd = 1;\n \n+\tif (startup_info->original_cwd)\n+\t\tcmd.dir = startup_info->original_cwd;\n \tstrvec_push(&cmd.args, \"checkout\");\n \tstrvec_push(&cmd.args, commit);\n \tstrvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex b1182390ba3..52335a8afe9 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rebase reverted\n+\ttest_incidental_dir_removal success git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git rebase fd_conflict\n+\ttest_required_dir_removal success git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"442752","messageId":"2855ed45112acc2a69a96a13784bff8de87b88d6.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 08/11] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:51Z","receivedAt":"2021-12-01T06:41:12Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince stash spawns a `clean` subprocess, make sure we run that from the\nstartup_info->original_cwd directory, so that the `clean` processs knows\nto protect that directory.  Also, since the `clean` command might no\nlonger run from the toplevel, pass the ':/' magic pathspec to ensure we\nstill clean from the toplevel.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 4 +++-\n t/t2501-cwd-empty.sh | 2 +-\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..de0e432a4ff 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (startup_info->original_cwd)\n+\t\t\t\tcp.dir = startup_info->original_cwd;\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 52335a8afe9..be9ef903bd4 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n+\ttest_incidental_untracked_dir_removal success \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n\n"},{"id":"442753","messageId":"23dfc3e399d4a66fc6ae7ad9cff477a95a125587.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 09/11] dir: avoid incidentally removing the original_cwd in remove_path()","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:52Z","receivedAt":"2021-12-01T06:41:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nModern git often tries to avoid leaving empty directories around when\nremoving files.  Originally, it did not bother.  This behavior started\nwith commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\ndirectories, 2005-11-19), stating the reason simply as:\n\n    When the last file in a directory is removed as the result of a\n    merge, try to rmdir the now-empty directory.\n\nThis was reimplemented in C and renamed to remove_path() in commit\ne1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\ninternal to merge-recursive.\n\nThis trend towards removing leading empty directories continued with\ncommit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\nstated the reasoning as:\n\n    The other question is what to do with leading directories. The old\n    \"git rm\" script didn't do anything, which is somewhat inconsistent.\n    This one will actually clean up directories that have become empty\n    as a result of removing the last file, but maybe we want to have a\n    flag to decide the behaviour?\n\nremove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\nfunction to remove as much as possible of a path, 2008-09-27), because\nit was noted that we had two separate implementations of the same idea\nAND both were buggy.  It described the purpose of the function as\n\n    a function to remove as much as possible of a path\n\nWhy remove as much as possible?  Well, at the time we probably would\nhave said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nBut I don't believe those reasons hold when the empty directory happens\nto be the current working directory we inherited from our parent\nprocess.  Leaving the parent process in a deleted directory can cause\nuser confusion when subsequent processes fail: any git command, for\nexample, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nOther commands may similarly get confused.  Modify remove_path() so that\nthe empty leading directories it also deletes does not include the\ncurrent working directory we inherited from our parent process.  I have\nlooked through every caller of remove_path() in the current codebase to\nmake sure that all should take this change.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                |  3 +++\n dir.h                |  6 +++++-\n t/t2501-cwd-empty.sh | 12 ++++--------\n 3 files changed, 12 insertions(+), 9 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..97d6b71c872 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3327,6 +3327,9 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/dir.h b/dir.h\nindex 83f46c0fb4c..d6a5d03bec2 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n  */\n int remove_dir_recursively(struct strbuf *path, int flag);\n \n-/* tries to remove the path with empty directories along it, ignores ENOENT */\n+/*\n+ * Tries to remove the path, along with leading empty directories so long as\n+ * those empty directories are not startup_info->original_cwd.  Ignores\n+ * ENOENT.\n+ */\n int remove_path(const char *path);\n \n int fspathcmp(const char *a, const char *b);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9ef903bd4..ce2efb9d30a 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -182,12 +182,12 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rm bar/baz.t\n+\ttest_incidental_dir_removal success git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal failure git apply ../patch\n+\ttest_incidental_dir_removal success git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n@@ -271,12 +271,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n \t) &&\n \n \ttest_path_is_missing a/b/c/tracked &&\n-\t## We would prefer if a/b was still present, though empty, since it\n-\t## was the current working directory\n-\t#test_path_is_dir a/b\n-\t## But the current behavior is that it not only deletes the directory\n-\t## a/b as requested, but also goes and deletes a\n-\ttest_path_is_missing a\n+\ttest_path_is_missing a/b/c &&\n+\ttest_path_is_dir a/b\n '\n \n test_expect_success 'git version continues working from a deleted dir' '\n-- \ngitgitgadget\n\n"},{"id":"442754","messageId":"fe47c0f0c174a7571a76149e144c036d99a46b98.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:53Z","receivedAt":"2021-12-01T06:41:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nremove_dir_recurse(), and its non-static wrapper called\nremove_dir_recursively(), both take flags for modifying its behavior.\nAs with the previous commits, we would generally like to protect\nthe original_cwd, but we want to forced user commands (e.g. 'git rm -rf\n...') or other special cases to remove it.  Add a flag for this purpose.\nAfter reading through every caller of remove_dir_recursively() in the\ncurrent codebase, there was only one that should be adjusted and that\none only in a very unusual circumstance.  Add a pair of new testcases to\nhighlight that very specific case involving submodules && --git-dir &&\n--work-tree.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/rm.c         |  3 ++-\n dir.c                | 12 +++++++++---\n dir.h                |  3 +++\n t/t2501-cwd-empty.sh |  5 -----\n 4 files changed, 14 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/rm.c b/builtin/rm.c\nindex 3d0967cdc11..b4132e5d8ee 100644\n--- a/builtin/rm.c\n+++ b/builtin/rm.c\n@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)\n \tif (!index_only) {\n \t\tint removed = 0, gitmodules_modified = 0;\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tint flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;\n \t\tfor (i = 0; i < list.nr; i++) {\n \t\t\tconst char *path = list.entry[i].name;\n \t\t\tif (list.entry[i].is_submodule) {\n \t\t\t\tstrbuf_reset(&buf);\n \t\t\t\tstrbuf_addstr(&buf, path);\n-\t\t\t\tif (remove_dir_recursively(&buf, 0))\n+\t\t\t\tif (remove_dir_recursively(&buf, flag))\n \t\t\t\t\tdie(_(\"could not remove '%s'\"), path);\n \n \t\t\t\tremoved = 1;\ndiff --git a/dir.c b/dir.c\nindex 97d6b71c872..52064345a6b 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tint ret = 0, original_len = path->len, len, kept_down = 0;\n \tint only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);\n \tint keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);\n+\tint purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);\n \tstruct object_id submodule_head;\n \n \tif ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&\n@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (!purge_original_cwd &&\n+\t\t    startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\ndiff --git a/dir.h b/dir.h\nindex d6a5d03bec2..8e02dfb505d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n /* Remove the contents of path, but leave path itself. */\n #define REMOVE_DIR_KEEP_TOPLEVEL 04\n \n+/* Remove the_original_cwd too */\n+#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08\n+\n /*\n  * Remove path and its contents, recursively. flags is a combination\n  * of the above REMOVE_DIR_* constants. Return 0 on success.\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex ce2efb9d30a..bc92230f2f2 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -291,11 +291,6 @@ test_submodule_removal () {\n \ttest_status=\n \ttest \"$path_status\" = dir && test_status=test_must_fail\n \n-\t# Actually, while path_status=dir && test_status=test_must_fail\n-\t# reflect our desired behavior, current behavior is:\n-\tpath_status=missing\n-\ttest_status=\n-\n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"442755","messageId":"431dd651a7e1a85e6d6ed430211089d00d425794.1638340854.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v5 11/11] t2501: simplify the tests since we can now assume desired behavior","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-01T06:40:54Z","receivedAt":"2021-12-01T06:41:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe no longer are dealing with a mixture of previous and desired\nbehavior, so simplify the tests a bit.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------\n 1 file changed, 36 insertions(+), 87 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex bc92230f2f2..f6d8d7d03d7 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -32,9 +32,6 @@ test_expect_success setup '\n '\n \n test_incidental_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -44,88 +41,57 @@ test_incidental_dir_removal () {\n \t\tcd foo &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure foo still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir foo\n-\telse\n-\t\ttest_path_is_missing foo\n-\tfi\n+\ttest_path_is_dir foo\n }\n \n test_required_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \tgit checkout df_conflict^{commit} &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We'd like for the command to fail (much as it would if there\n-\t\t# was an untracked file there), and for the index and worktree\n-\t\t# to be left clean with pwd and git status working afterwards.\n-\t\t# But test for existing rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest_must_fail \"$@\" 2>../error &&\n-\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n-\n-\t\t\tgit diff --exit-code HEAD &&\n-\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t\"$@\" &&\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Ensure command refuses to run\n+\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t# ...and that the index and working tree are left clean\n+\t\tgit diff --exit-code HEAD &&\n+\n+\t\t# Ensure that getcwd and git status do not error out (which\n+\t\t# they might if the current working directory had been removed)\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \n-\t# Although we want dirORfile to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir dirORfile\n-\telse\n-\t\ttest_path_is_file dirORfile\n-\tfi\n+\ttest_path_is_dir dirORfile\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git checkout init\n+\ttest_incidental_dir_removal git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git checkout fd_conflict\n+\ttest_required_dir_removal git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git reset --hard init\n+\ttest_incidental_dir_removal git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git reset --hard fd_conflict\n+\ttest_required_dir_removal git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git merge reverted\n+\ttest_incidental_dir_removal git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -154,46 +120,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git merge fd_conflict\n+\ttest_required_dir_removal git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git cherry-pick reverted\n+\ttest_incidental_dir_removal git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git cherry-pick fd_conflict\n+\ttest_required_dir_removal git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rebase reverted\n+\ttest_incidental_dir_removal git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git rebase fd_conflict\n+\ttest_required_dir_removal git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git revert HEAD\n+\ttest_incidental_dir_removal git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git revert undo_fd_conflict\n+\ttest_required_dir_removal git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rm bar/baz.t\n+\ttest_incidental_dir_removal git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal success git apply ../patch\n+\ttest_incidental_dir_removal git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -205,38 +168,24 @@ test_incidental_untracked_dir_removal () {\n \t\tcd untracked &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure untracked still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing empty &&\n \ttest_path_is_missing untracked/random &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir untracked\n-\telse\n-\t\ttest_path_is_missing untracked\n-\tfi\n+\ttest_path_is_dir untracked\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n \tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n"},{"id":"443291","messageId":"aa85e35d-143e-93e4-f54b-146b38dd4b88@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-12-07T16:09:24Z","receivedAt":"2021-12-07T16:09:29Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:\n> Traditionally, if folks run git commands such as checkout or rebase from a\n> subdirectory, that git command could remove their current working directory\n> and result in subsequent git and non-git commands either getting confused or\n> printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> working directory: No such file or directory\"). Many commands either\n> silently avoid removing directories that are not empty (i.e. those that have\n> untracked or modified files in them)[1], or show an error and abort,\n> depending on which is more appropriate for the command in question. With\n> this series, we augment the reasons to avoid removing directories to include\n> not just has-untracked-or-modified-files, but also to avoid removing the\n> original_cwd as well.\n\nI did not clearly voice my approval of the core idea here, but I do like it.\n\nI think this fits squarely into a category of \"help the user not get stuck\"\nwhich Git has enough of those situations that we don't need this one. Even\nexpert users won't know for sure if a 'git checkout' will cause their current\ndirectory to be removed, however unlikely.\n\nIn the Git project, we spend a lot of time in the root of our workdir, but\nthis is not the typical case for large projects. I remember spending most of\nmy time in a previous role working four levels deep in the directory hierarchy.\n\n\nI read the previous two range-diffs and took another pass at this v5 and\ndidn't see anything worth commenting on. This version is good to go.\n\nThere is _also_ more work to do, as follow-ups. In particular, the thing\nthat I thought about was sparse-checkout and created this test which still\nfails at the end of your series (as an addition to t1092)\n\ntest_expect_success 'remove cwd' '\n\tinit_repos &&\n\n\ttest_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n\tfor repo in sparse-checkout sparse-index\n\tdo\n\t\t(\n\t\t\tcd $repo/deep/deeper1 &&\n\t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n\t\t\tgit sparse-checkout set &&\n\n\t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n\t\t\ttest_sparse_match git status --porcelain &&\n\t\t\tcd \"$TRASH_DIRECTORY\" &&\n\t\t\ttest_cmp expect actual\n\t\t)\n\tdone\n'\n\nPlease do not let this test delay the advancement of this series. As we\nfind these kinds of issues, we can fix them one-by-one as needed.\n\nThanks,\n-Stolee\n"},{"id":"443335","messageId":"211207.86tufkmfc2.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"aa85e35d-143e-93e4-f54b-146b38dd4b88@gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-07T18:30:23Z","receivedAt":"2021-12-07T18:37:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 07 2021, Derrick Stolee wrote:\n\n> On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:\n>> Traditionally, if folks run git commands such as checkout or rebase from a\n>> subdirectory, that git command could remove their current working directory\n>> and result in subsequent git and non-git commands either getting confused or\n>> printing messages that confuse the user (e.g. \"fatal: Unable to read current\n>> working directory: No such file or directory\"). Many commands either\n>> silently avoid removing directories that are not empty (i.e. those that have\n>> untracked or modified files in them)[1], or show an error and abort,\n>> depending on which is more appropriate for the command in question. With\n>> this series, we augment the reasons to avoid removing directories to include\n>> not just has-untracked-or-modified-files, but also to avoid removing the\n>> original_cwd as well.\n>\n> I did not clearly voice my approval of the core idea here, but I do like it.\n>\n> I think this fits squarely into a category of \"help the user not get stuck\"\n> which Git has enough of those situations that we don't need this one. Even\n> expert users won't know for sure if a 'git checkout' will cause their current\n> directory to be removed, however unlikely.\n>\n> In the Git project, we spend a lot of time in the root of our workdir, but\n> this is not the typical case for large projects. I remember spending most of\n> my time in a previous role working four levels deep in the directory hierarchy.\n>\n>\n> I read the previous two range-diffs and took another pass at this v5 and\n> didn't see anything worth commenting on. This version is good to go.\n>\n> There is _also_ more work to do, as follow-ups. In particular, the thing\n> that I thought about was sparse-checkout and created this test which still\n> fails at the end of your series (as an addition to t1092)\n>\n> test_expect_success 'remove cwd' '\n> \tinit_repos &&\n>\n> \ttest_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n> \tfor repo in sparse-checkout sparse-index\n> \tdo\n> \t\t(\n> \t\t\tcd $repo/deep/deeper1 &&\n> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n> \t\t\tgit sparse-checkout set &&\n>\n> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n> \t\t\ttest_sparse_match git status --porcelain &&\n> \t\t\tcd \"$TRASH_DIRECTORY\" &&\n> \t\t\ttest_cmp expect actual\n> \t\t)\n> \tdone\n> '\n>\n> Please do not let this test delay the advancement of this series. As we\n> find these kinds of issues, we can fix them one-by-one as needed.\n\nNot to pile on about \"the core idea\", just a question while this is\nfresh in your mind:\n\nI think that those cases would per [1] be ones where a more isolated\nchange of reading the $PWD from the environment would make all those\ncommands work as expected. Or would the \"$TRASH_DIRECTORY\" also\notherwise go away in this examples?\n\nAnyway, just per [1] and the potential future follow-ups is this (I\ndon't think so, but maybe I'm wrong) or other examples you have things\nthat specifically need the \"retain the getcwd()\" part of this series?\n\nOr just (as I think would be the case with that \"git status\") to not\nhave setup.c die quite as eagerly as it does now when getcwd() fails,\nbut it can find its way back to the .git via the environment's $PWD?\n\nAlso note that writing any tests to check the current behavior is a pain\ndue to the bin-wrappers as I discussed in some previous threads, but\nthat's a test-only oddity, and won't impact how git would behave once\ninstalled (although now it doesn't behave well at all). So probing the\ncurrent behavior via tests is hard, unless you use GIT_TEST_INSTALLED to\nget around the bin-wrappers.\n\nThere *are* definitely cases where not-just-that-setup.c change but also\nthe \"don't remove the CWD\" is an inherently better & more complete\nsolution. But I think that's mainly to do with 3rd party shellscripts &\nother programs outside of our control.\n\nI'm assuming that you were working with this on Windows, where\npresumably there's fewer/none such shellscripts you rely on, but that's\nnow two presumes in a row, so... :)\n\n1. https://lore.kernel.org/git/211207.86ee6opy0f.gmgdl@evledraar.gmail.com/\n"},{"id":"443365","messageId":"CABPp-BHRse4BwXTjd4cRruOdymt=DJRPDL1jyPzX5HDqx4ymDw@mail.gmail.com","threadId":"56952","inReplyTo":"aa85e35d-143e-93e4-f54b-146b38dd4b88@gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2021-12-07T20:43:03Z","receivedAt":"2021-12-07T20:43:18Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 7, 2021 at 8:09 AM Derrick Stolee <stolee@gmail.com> wrote:\n>\n> On 12/1/2021 1:40 AM, Elijah Newren via GitGitGadget wrote:\n> > Traditionally, if folks run git commands such as checkout or rebase from a\n> > subdirectory, that git command could remove their current working directory\n> > and result in subsequent git and non-git commands either getting confused or\n> > printing messages that confuse the user (e.g. \"fatal: Unable to read current\n> > working directory: No such file or directory\"). Many commands either\n> > silently avoid removing directories that are not empty (i.e. those that have\n> > untracked or modified files in them)[1], or show an error and abort,\n> > depending on which is more appropriate for the command in question. With\n> > this series, we augment the reasons to avoid removing directories to include\n> > not just has-untracked-or-modified-files, but also to avoid removing the\n> > original_cwd as well.\n>\n> I did not clearly voice my approval of the core idea here, but I do like it.\n>\n> I think this fits squarely into a category of \"help the user not get stuck\"\n> which Git has enough of those situations that we don't need this one. Even\n> expert users won't know for sure if a 'git checkout' will cause their current\n> directory to be removed, however unlikely.\n>\n> In the Git project, we spend a lot of time in the root of our workdir, but\n> this is not the typical case for large projects. I remember spending most of\n> my time in a previous role working four levels deep in the directory hierarchy.\n>\n>\n> I read the previous two range-diffs and took another pass at this v5 and\n> didn't see anything worth commenting on. This version is good to go.\n>\n> There is _also_ more work to do, as follow-ups. In particular, the thing\n> that I thought about was sparse-checkout and created this test which still\n> fails at the end of your series (as an addition to t1092)\n\nInteresting testcase...\n\n> test_expect_success 'remove cwd' '\n>         init_repos &&\n>\n>         test_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n>         for repo in sparse-checkout sparse-index\n>         do\n>                 (\n>                         cd $repo/deep/deeper1 &&\n>                         test-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n>                         git sparse-checkout set &&\n>\n>                         test-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n>                         test_sparse_match git status --porcelain &&\n\nHowever, this line is broken even if the directory weren't removed.\nNot because of the \"git status --porcelain\" part, but because of two\nother reasons:\n\n1) test_sparse_match presumes it is run from the directory above the\nrepos while you still have it in $repo/deep/deeper1\n2) The point of test_sparse_match is to compare the results in two\ndifferent repositories, but it'll do this twice and the first time\nwithout the changes having been made to the sparse-index repo.\nPerhaps this belonged outside the surrounding for loop?\n\nI think I'd either drop the \"test_sparse_match\" or else just drop the\nwhole line; the real comparison is the expect/actual files.  Dropping\nthis line makes it a good test.\n\n>                         cd \"$TRASH_DIRECTORY\" &&\n>                         test_cmp expect actual\n>                 )\n>         done\n> '\n>\n> Please do not let this test delay the advancement of this series. As we\n> find these kinds of issues, we can fix them one-by-one as needed.\n\nYeah, sounds good.  Since you piqued my interest, though, the problem\nis that we're passing an absolute path to remove_dir_recursively()\ninside clean_tracked_sparse_directories() when we should be passing a\nrelative path.  (We always chdir(r->worktree) in setup.c, so there's\nno need to prepend the path with r->worktree+'/'.)\n\nStill, the current series is long enough and unless there are issues\nothers have spotted with it, I'd rather just let it proceed as-is and\nthen send this fix and a correction of your testcase in separately.\n"},{"id":"443367","messageId":"31bef4f9-2313-83a2-14e9-4ed1cc73a59c@gmail.com","threadId":"56952","inReplyTo":"211207.86tufkmfc2.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-12-07T20:57:50Z","receivedAt":"2021-12-07T20:57:55Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 12/7/2021 1:30 PM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Tue, Dec 07 2021, Derrick Stolee wrote:\n>> test_expect_success 'remove cwd' '\n>> \tinit_repos &&\n>>\n>> \ttest_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n>> \tfor repo in sparse-checkout sparse-index\n>> \tdo\n>> \t\t(\n>> \t\t\tcd $repo/deep/deeper1 &&\n>> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n>> \t\t\tgit sparse-checkout set &&\n>>\n>> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n>> \t\t\ttest_sparse_match git status --porcelain &&\n>> \t\t\tcd \"$TRASH_DIRECTORY\" &&\n>> \t\t\ttest_cmp expect actual\n>> \t\t)\n>> \tdone\n>> '\n>>\n>> Please do not let this test delay the advancement of this series. As we\n>> find these kinds of issues, we can fix them one-by-one as needed.\n> \n> Not to pile on about \"the core idea\", just a question while this is\n> fresh in your mind:\n> \n> I think that those cases would per [1] be ones where a more isolated\n> change of reading the $PWD from the environment would make all those\n> commands work as expected. Or would the \"$TRASH_DIRECTORY\" also\n> otherwise go away in this examples?\n\nI have read this message and the one you are referring two twice and\nI cannot understand what you are trying to say here.\n\n> Anyway, just per [1] and the potential future follow-ups is this (I\n> don't think so, but maybe I'm wrong) or other examples you have things\n> that specifically need the \"retain the getcwd()\" part of this series?\n> \n> Or just (as I think would be the case with that \"git status\") to not\n> have setup.c die quite as eagerly as it does now when getcwd() fails,\n> but it can find its way back to the .git via the environment's $PWD?\n\nAre you implying that Git will be the only thing broken by a missing\ndirectory after we leave in this state? I doubt that is true, and we\nshould be good citizens here by leaving the directory around.\n\n> There *are* definitely cases where not-just-that-setup.c change but also\n> the \"don't remove the CWD\" is an inherently better & more complete\n> solution. But I think that's mainly to do with 3rd party shellscripts &\n> other programs outside of our control.\n\nExactly. We should take this change because it is valuable to not cause\na confusing error in other tools.\n \n> I'm assuming that you were working with this on Windows, where\n> presumably there's fewer/none such shellscripts you rely on, but that's\n> now two presumes in a row, so... :)\n\nI'm working on Ubuntu, where I do all of my Git development unless there\nis a platform specific reason to do so.\n\nThanks,\n-Stolee\n"},{"id":"443368","messageId":"a6730006-bc30-d1c6-2d9a-c00df505d38d@gmail.com","threadId":"56952","inReplyTo":"CABPp-BHRse4BwXTjd4cRruOdymt=DJRPDL1jyPzX5HDqx4ymDw@mail.gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-12-07T21:00:01Z","receivedAt":"2021-12-07T21:00:36Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 12/7/2021 3:43 PM, Elijah Newren wrote:\n> On Tue, Dec 7, 2021 at 8:09 AM Derrick Stolee <stolee@gmail.com> wrote:\n>> There is _also_ more work to do, as follow-ups. In particular, the thing\n>> that I thought about was sparse-checkout and created this test which still\n>> fails at the end of your series (as an addition to t1092)\n> \n> Interesting testcase...\n> \n>> test_expect_success 'remove cwd' '\n>>         init_repos &&\n>>\n>>         test_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n>>         for repo in sparse-checkout sparse-index\n>>         do\n>>                 (\n>>                         cd $repo/deep/deeper1 &&\n>>                         test-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n>>                         git sparse-checkout set &&\n>>\n>>                         test-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n>>                         test_sparse_match git status --porcelain &&\n> \n> However, this line is broken even if the directory weren't removed.\n> Not because of the \"git status --porcelain\" part, but because of two\n> other reasons:\n> \n> 1) test_sparse_match presumes it is run from the directory above the\n> repos while you still have it in $repo/deep/deeper1\n> 2) The point of test_sparse_match is to compare the results in two\n> different repositories, but it'll do this twice and the first time\n> without the changes having been made to the sparse-index repo.\n> Perhaps this belonged outside the surrounding for loop?\n\nYou're right! I had first written this test with test_sparse_match\neverywhere, but test_sparse_match uses subshells to 'cd' into different\nplaces, so it doesn't let us get a failure in the 'test-tool getcwd'.\n\nI missed this one because the test fails with 'test-tool getcwd' so I\nnever get to the broken line.\n\n> I think I'd either drop the \"test_sparse_match\" or else just drop the\n> whole line; the real comparison is the expect/actual files.  Dropping\n> this line makes it a good test.\n\nI agree.\n\n>>                         cd \"$TRASH_DIRECTORY\" &&\n>>                         test_cmp expect actual\n>>                 )\n>>         done\n>> '\n>>\n>> Please do not let this test delay the advancement of this series. As we\n>> find these kinds of issues, we can fix them one-by-one as needed.\n> \n> Yeah, sounds good.  Since you piqued my interest, though, the problem\n> is that we're passing an absolute path to remove_dir_recursively()\n> inside clean_tracked_sparse_directories() when we should be passing a\n> relative path.  (We always chdir(r->worktree) in setup.c, so there's\n> no need to prepend the path with r->worktree+'/'.)\n> \n> Still, the current series is long enough and unless there are issues\n> others have spotted with it, I'd rather just let it proceed as-is and\n> then send this fix and a correction of your testcase in separately.\n\nAbsolutely. Let's pick up this fix another time.\n\nThanks,\n-Stolee\n"},{"id":"443413","messageId":"211208.86a6hbmlvj.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"31bef4f9-2313-83a2-14e9-4ed1cc73a59c@gmail.com","subject":"Re: [PATCH v5 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-08T10:23:56Z","receivedAt":"2021-12-08T10:28:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Dec 07 2021, Derrick Stolee wrote:\n\n> On 12/7/2021 1:30 PM, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> On Tue, Dec 07 2021, Derrick Stolee wrote:\n>>> test_expect_success 'remove cwd' '\n>>> \tinit_repos &&\n>>>\n>>> \ttest_sparse_match git sparse-checkout set deep/deeper1/deepest &&\n>>> \tfor repo in sparse-checkout sparse-index\n>>> \tdo\n>>> \t\t(\n>>> \t\t\tcd $repo/deep/deeper1 &&\n>>> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/expect\" &&\n>>> \t\t\tgit sparse-checkout set &&\n>>>\n>>> \t\t\ttest-tool getcwd >\"$TRASH_DIRECTORY/actual\" &&\n>>> \t\t\ttest_sparse_match git status --porcelain &&\n>>> \t\t\tcd \"$TRASH_DIRECTORY\" &&\n>>> \t\t\ttest_cmp expect actual\n>>> \t\t)\n>>> \tdone\n>>> '\n>>>\n>>> Please do not let this test delay the advancement of this series. As we\n>>> find these kinds of issues, we can fix them one-by-one as needed.\n>> \n>> Not to pile on about \"the core idea\", just a question while this is\n>> fresh in your mind:\n>> \n>> I think that those cases would per [1] be ones where a more isolated\n>> change of reading the $PWD from the environment would make all those\n>> commands work as expected. Or would the \"$TRASH_DIRECTORY\" also\n>> otherwise go away in this examples?\n>\n> I have read this message and the one you are referring two twice and\n> I cannot understand what you are trying to say here.\n\nI'm asking whether the WIP patch I posted at\nhttps://lore.kernel.org/git/211124.86sfvld4cl.gmgdl@evledraar.gmail.com/\nwould partially/entirely solve those issues you mentiened (but I see\nElijah has a side-reply saying it might not in either approach).\n\n>> Anyway, just per [1] and the potential future follow-ups is this (I\n>> don't think so, but maybe I'm wrong) or other examples you have things\n>> that specifically need the \"retain the getcwd()\" part of this series?\n>> \n>> Or just (as I think would be the case with that \"git status\") to not\n>> have setup.c die quite as eagerly as it does now when getcwd() fails,\n>> but it can find its way back to the .git via the environment's $PWD?\n>\n> Are you implying that Git will be the only thing broken by a missing\n> directory after we leave in this state? I doubt that is true, and we\n> should be good citizens here by leaving the directory around.\n\nNo, I'm just trying to clarify which specific thing we'd need in\nthis/other cases to fix things *in git*. Because...\n\n>> There *are* definitely cases where not-just-that-setup.c change but also\n>> the \"don't remove the CWD\" is an inherently better & more complete\n>> solution. But I think that's mainly to do with 3rd party shellscripts &\n>> other programs outside of our control.\n>\n> Exactly. We should take this change because it is valuable to not cause\n> a confusing error in other tools.\n\n...maybe we should fix things for other tools too, but for any future\ndevelopment it helps to know what was needed to fix what issues.\n\n>> I'm assuming that you were working with this on Windows, where\n>> presumably there's fewer/none such shellscripts you rely on, but that's\n>> now two presumes in a row, so... :)\n>\n> I'm working on Ubuntu, where I do all of my Git development unless there\n> is a platform specific reason to do so.\n\n*nod*, badly assumed then :)\n"},{"id":"443569","messageId":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v5.git.git.1638340854.gitgitgadget@gmail.com","subject":"[PATCH v6 00/11] Avoid removing the current working directory, even if it becomes empty","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:24Z","receivedAt":"2021-12-09T05:08:41Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Traditionally, if folks run git commands such as checkout or rebase from a\nsubdirectory, that git command could remove their current working directory\nand result in subsequent git and non-git commands either getting confused or\nprinting messages that confuse the user (e.g. \"fatal: Unable to read current\nworking directory: No such file or directory\"). Many commands either\nsilently avoid removing directories that are not empty (i.e. those that have\nuntracked or modified files in them)[1], or show an error and abort,\ndepending on which is more appropriate for the command in question. With\nthis series, we augment the reasons to avoid removing directories to include\nnot just has-untracked-or-modified-files, but also to avoid removing the\noriginal_cwd as well.\n\nPeff and Junio provided some good pros/cons, if it helps:\n\n * Pros: Peff (original suggester of the idea)[2], and Junio[3]\n * Cons: Peff [2, again -- see the \"P.S.\"], and Junio[4]\n\n[1] well, with a few exceptions; see\nhttps://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n[2] https://lore.kernel.org/git/YS8eEtwQvF7TaLCb@coredump.intra.peff.net/\n[3] https://lore.kernel.org/git/xmqqo86elyht.fsf@gitster.g/ [4]\nhttps://lore.kernel.org/git/xmqqo8691gr8.fsf@gitster.g/\n\nChanges since v5:\n\n * It's been about a week without changes, so including Acks that have come\n   in (let me know if I was wrong in my interpretation of these statements\n   as Acks):\n   * from Stolee (\"This version is good to go\",\n     https://lore.kernel.org/git/aa85e35d-143e-93e4-f54b-146b38dd4b88@gmail.com/)\n   * from Ævar (\"I've got no objections to these changes going in as they\n     stand.\",\n     https://lore.kernel.org/git/211207.86ee6opy0f.gmgdl@evledraar.gmail.com/)\n\nChanges since v4:\n\n * actually fix bashism\n\nChanges since v3:\n\n * fixed one codepath from v2 so that the series really is only about the\n   working tree\n * used test-tool getcwd instead of pwd -P as suggested by Ævar for some\n   less common platforms\n * fixed bashism\n * check for clean index/worktree after verifying that expected-to-abort\n   codepaths do abort, to make it clearer that we expect an early abort\n * remove a leftover (and confusing) is_absolute_dir() check in sequencer\n   and stash from an earlier round of the series\n\nChanges since v2:\n\n * the series is now only about the working tree. So if the original cwd is\n   outside the worktree (or we're in a bare repo), then the new code is a\n   no-op.\n * fixed ugly early die() possibility (uses strbuf_getcwd() instead of\n   xgetcwd())\n * modified the initial tests to show both expected and desired behavior.\n   subsequent patches fix the tests. One new patch added at the end which\n   simplifies the tests to only check for desired behavior.\n * NULLify startup_info->original_cwd when it matches the toplevel worktree;\n   that is already protected and we don't need secondary protection for it.\n   This simplified some other codepaths so we don't have to check for\n   startup_info->original_cwd == \"\".\n * clarified some commit messages\n\nChanges since v1:\n\n * clarified multiple commit messages\n * renamed the_cwd to startup_info->original_cwd to make it clearer that\n   it's our parent process'es cwd that really matters, which we inherited at\n   program startup. Also pulls it out of the global namespace.\n * Normalize the path for startup_info->original_cwd, and ensure that it's\n   actually the original cwd even if -C is passed to git.\n * small code cleanups suggested by René and Ævar\n * split the final patch (which got the most comments) into two -- one for\n   each function being modified. Also, add a bunch more history to the first\n   of the two resulting commit messages\n * no longer has a content conflict with so/stash-staged\n * add another value for the flags parameter that remove_dir_recursively()\n   takes so that it can opt into either the old or the new behavior. Use\n   that for the one special corner case I could find where it matters, and\n   add a few tests around it to highlight the utility of the flag.\n\nElijah Newren (11):\n  t2501: add various tests for removing the current working directory\n  setup: introduce startup_info->original_cwd\n  unpack-trees: refuse to remove startup_info->original_cwd\n  unpack-trees: add special cwd handling\n  symlinks: do not include startup_info->original_cwd in dir removal\n  clean: do not attempt to remove startup_info->original_cwd\n  rebase: do not attempt to remove startup_info->original_cwd\n  stash: do not attempt to remove startup_info->original_cwd\n  dir: avoid incidentally removing the original_cwd in remove_path()\n  dir: new flag to remove_dir_recurse() to spare the original_cwd\n  t2501: simplify the tests since we can now assume desired behavior\n\n builtin/clean.c      |  44 +++++--\n builtin/rm.c         |   3 +-\n builtin/stash.c      |   4 +-\n cache.h              |   2 +\n common-main.c        |   4 +\n dir.c                |  15 ++-\n dir.h                |   9 +-\n sequencer.c          |   2 +\n setup.c              |  65 ++++++++++\n symlinks.c           |   8 +-\n t/t2501-cwd-empty.sh | 277 +++++++++++++++++++++++++++++++++++++++++++\n unpack-trees.c       |  30 ++++-\n unpack-trees.h       |   1 +\n 13 files changed, 442 insertions(+), 22 deletions(-)\n create mode 100755 t/t2501-cwd-empty.sh\n\n\nbase-commit: 88d915a634b449147855041d44875322de2b286d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1140%2Fnewren%2Fcwd_removal-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1140/newren/cwd_removal-v6\nPull-Request: https://github.com/git/git/pull/1140\n\nRange-diff vs v5:\n\n  1:  7c72e888d2e !  1:  1fd76d4f8a8 t2501: add various tests for removing the current working directory\n     @@ Commit message\n          Also add a few tests suggested during the review of earlier rounds of\n          this patch series.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh (new) ##\n  2:  37f333b2024 !  2:  cd1f564c4c0 setup: introduce startup_info->original_cwd\n     @@ Commit message\n      \n          Subsequent commits will make use of this new variable.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## cache.h ##\n  3:  b611c73bd15 !  3:  4a1f62b662c unpack-trees: refuse to remove startup_info->original_cwd\n     @@ Commit message\n          error out if the directory is the current working directory we inherited\n          from our parent process.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh ##\n  4:  706415a4547 !  4:  03f2b0b8fb7 unpack-trees: add special cwd handling\n     @@ Commit message\n          there is no untracked checking to be done, so we simply add a special\n          case near the top of verify_absent_1.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh ##\n  5:  66ef6b4d943 !  5:  e75f6c3f9ad symlinks: do not include startup_info->original_cwd in dir removal\n     @@ Commit message\n          commands) that would otherwise report confusing messages about being\n          unable to read the current working directory.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## symlinks.c ##\n  6:  54d059c683e !  6:  d4f8784a6e4 clean: do not attempt to remove startup_info->original_cwd\n     @@ Metadata\n       ## Commit message ##\n          clean: do not attempt to remove startup_info->original_cwd\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/clean.c ##\n  7:  f8efb7446c3 !  7:  fd500cc1843 rebase: do not attempt to remove startup_info->original_cwd\n     @@ Commit message\n          the startup_info->original_cwd directory, so that the checkout process\n          knows to protect that directory.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## sequencer.c ##\n  8:  2855ed45112 !  8:  6f3c4fd49b9 stash: do not attempt to remove startup_info->original_cwd\n     @@ Commit message\n          longer run from the toplevel, pass the ':/' magic pathspec to ensure we\n          still clean from the toplevel.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/stash.c ##\n  9:  23dfc3e399d !  9:  1a66b1a2386 dir: avoid incidentally removing the original_cwd in remove_path()\n     @@ Commit message\n          looked through every caller of remove_path() in the current codebase to\n          make sure that all should take this change.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## dir.c ##\n 10:  fe47c0f0c17 ! 10:  4a18efd51ef dir: new flag to remove_dir_recurse() to spare the original_cwd\n     @@ Commit message\n          highlight that very specific case involving submodules && --git-dir &&\n          --work-tree.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## builtin/rm.c ##\n 11:  431dd651a7e ! 11:  63781ed8b8e t2501: simplify the tests since we can now assume desired behavior\n     @@ Commit message\n          We no longer are dealing with a mixture of previous and desired\n          behavior, so simplify the tests a bit.\n      \n     +    Acked-by: Derrick Stolee <stolee@gmail.com>\n     +    Acked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n          Signed-off-by: Elijah Newren <newren@gmail.com>\n      \n       ## t/t2501-cwd-empty.sh ##\n\n-- \ngitgitgadget\n"},{"id":"443570","messageId":"1fd76d4f8a8c98d3c78c94cc8f3d36c335bd3bfb.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 01/11] t2501: add various tests for removing the current working directory","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:25Z","receivedAt":"2021-12-09T05:08:42Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nNumerous commands will remove directories left empty as a \"convenience\"\nafter removing files within them.  That is normally fine, but removing\nthe current working directory can be rather inconvenient since it can\ncause confusion for the user when they run subsequent commands.  For\nexample, after one git process has removed the current working\ndirectory, git status/log/diff will all abort with the message:\n\n    fatal: Unable to read current working directory: No such file or directory\n\nWe also have code paths that, when a file needs to be placed where a\ndirectory is (due to e.g. checkout, merge, reset, whatever), will check\nif this is okay and error out if not.  These rules include:\n  * all tracked files under that directory are intended to be removed by\n    the operation\n  * none of the tracked files under that directory have uncommitted\n    modification\n  * there are no untracked files under that directory\nHowever, if we end up remove the current working directory, we can cause\nuser confusion when they run subsequent commands, so we would prefer if\nthere was a fourth rule added to this list: avoid removing the current\nworking directory.\n\nSince there are several code paths that can result in the current\nworking directory being removed, add several tests of various different\ncodepaths.  To make it clearer what the difference between the current\nbehavior and the behavior at the end of the series, code both of them\ninto the tests and have the appropriate behavior be selected by a flag.\nSubsequent commits will toggle the flag from current to desired\nbehavior.\n\nAlso add a few tests suggested during the review of earlier rounds of\nthis patch series.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 342 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 342 insertions(+)\n create mode 100755 t/t2501-cwd-empty.sh\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nnew file mode 100755\nindex 00000000000..a05abd18187\n--- /dev/null\n+++ b/t/t2501-cwd-empty.sh\n@@ -0,0 +1,342 @@\n+#!/bin/sh\n+\n+test_description='Test handling of the current working directory becoming empty'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit init &&\n+\n+\tgit branch fd_conflict &&\n+\n+\tmkdir -p foo/bar &&\n+\ttest_commit foo/bar/baz &&\n+\n+\tgit revert HEAD &&\n+\tgit tag reverted &&\n+\n+\tgit checkout fd_conflict &&\n+\tmkdir dirORfile &&\n+\ttest_commit dirORfile/foo &&\n+\n+\tgit rm -r dirORfile &&\n+\techo not-a-directory >dirORfile &&\n+\tgit add dirORfile &&\n+\tgit commit -m dirORfile &&\n+\n+\tgit switch -c df_conflict HEAD~1 &&\n+\ttest_commit random_file &&\n+\n+\tgit switch -c undo_fd_conflict fd_conflict &&\n+\tgit revert HEAD\n+'\n+\n+test_incidental_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\ttest_path_is_dir foo/bar &&\n+\n+\t(\n+\t\tcd foo &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing foo/bar/baz &&\n+\ttest_path_is_missing foo/bar &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir foo\n+\telse\n+\t\ttest_path_is_missing foo\n+\tfi\n+}\n+\n+test_required_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\tgit checkout df_conflict^{commit} &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We'd like for the command to fail (much as it would if there\n+\t\t# was an untracked file there), and for the index and worktree\n+\t\t# to be left clean with pwd and git status working afterwards.\n+\t\t# But test for existing rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t\tgit diff --exit-code HEAD &&\n+\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t\"$@\" &&\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\n+\t# Although we want dirORfile to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir dirORfile\n+\telse\n+\t\ttest_path_is_file dirORfile\n+\tfi\n+}\n+\n+test_expect_success 'checkout does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git checkout init\n+'\n+\n+test_expect_success 'checkout fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git checkout fd_conflict\n+'\n+\n+test_expect_success 'reset --hard does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git reset --hard init\n+'\n+\n+test_expect_success 'reset --hard fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git reset --hard fd_conflict\n+'\n+\n+test_expect_success 'merge does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git merge reverted\n+'\n+\n+# This file uses some simple merges where\n+#   Base: 'dirORfile/' exists\n+#   Side1: random other file changed\n+#   Side2: 'dirORfile/' removed, 'dirORfile' added\n+# this should resolve cleanly, but merge-recursive throws merge conflicts\n+# because it's dumb.  Add a special test for checking merge-recursive (and\n+# merge-ort), then after this just hard require ort for all remaining tests.\n+#\n+test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' '\n+\tgit checkout foo/bar/baz &&\n+\ttest_when_finished \"git clean -fdx\" &&\n+\n+\tmkdir dirORfile &&\n+\t(\n+\t\tcd dirORfile &&\n+\n+\t\t# We would rather this failed, but we test for existing\n+\t\t# rather than desired behavior\n+\t\tgit merge fd_conflict 2>../error\n+\t) &&\n+\n+\t## Here is the behavior we would rather have:\n+\t#test_path_is_dir dirORfile &&\n+\t#grep \"Refusing to remove the current working directory\" error\n+\t## But instead we test for existing behavior\n+\ttest_path_is_file dirORfile &&\n+\ttest_must_be_empty error\n+'\n+\n+GIT_TEST_MERGE_ALGORITHM=ort\n+\n+test_expect_success 'merge fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git merge fd_conflict\n+'\n+\n+test_expect_success 'cherry-pick does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git cherry-pick reverted\n+'\n+\n+test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+'\n+\n+test_expect_success 'rebase does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rebase reverted\n+'\n+\n+test_expect_success 'rebase fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git rebase fd_conflict\n+'\n+\n+test_expect_success 'revert does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git revert HEAD\n+'\n+\n+test_expect_success 'revert fails if cwd needs to be removed' '\n+\ttest_required_dir_removal failure git revert undo_fd_conflict\n+'\n+\n+test_expect_success 'rm does not clean cwd incidentally' '\n+\ttest_incidental_dir_removal failure git rm bar/baz.t\n+'\n+\n+test_expect_success 'apply does not remove cwd incidentally' '\n+\tgit diff HEAD HEAD~1 >patch &&\n+\ttest_incidental_dir_removal failure git apply ../patch\n+'\n+\n+test_incidental_untracked_dir_removal () {\n+\tworks=$1 &&\n+\tshift &&\n+\n+\ttest_when_finished \"git reset --hard\" &&\n+\n+\tgit checkout foo/bar/baz^{commit} &&\n+\tmkdir -p untracked &&\n+\tmkdir empty\n+\t>untracked/random &&\n+\n+\t(\n+\t\tcd untracked &&\n+\t\t\"$@\" &&\n+\n+\t\t# Although we want pwd & git status to pass, test for existing\n+\t\t# rather than desired behavior.\n+\t\tif test \"$works\" = \"success\"\n+\t\tthen\n+\t\t\ttest-tool getcwd &&\n+\t\t\tgit status --porcelain\n+\t\telse\n+\t\t\t! test-tool getcwd &&\n+\t\t\ttest_might_fail git status --porcelain\n+\t\tfi\n+\t) &&\n+\ttest_path_is_missing empty &&\n+\ttest_path_is_missing untracked/random &&\n+\n+\t# Although we want dir to be present, test for existing rather\n+\t# than desired behavior.\n+\tif test \"$works\" = \"success\"\n+\tthen\n+\t\ttest_path_is_dir untracked\n+\telse\n+\t\ttest_path_is_missing untracked\n+\tfi\n+}\n+\n+test_expect_success 'clean does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings\n+'\n+\n+test_expect_success 'stash does not remove cwd incidentally' '\n+\ttest_incidental_untracked_dir_removal failure \\\n+\t\tgit stash --include-untracked\n+'\n+\n+test_expect_success '`rm -rf dir` only removes a subset of dir' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/untracked &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_dir a/b &&\n+\ttest_path_is_missing a/b/c/tracked &&\n+\ttest_path_is_file a/b/c/untracked\n+'\n+\n+test_expect_success '`rm -rf dir` even with only tracked files will remove something else' '\n+\ttest_when_finished \"rm -rf a/\" &&\n+\n+\tmkdir -p a/b/c &&\n+\t>a/b/c/tracked &&\n+\tgit add a/b/c/tracked &&\n+\n+\t(\n+\t\tcd a/b &&\n+\t\tgit rm -rf ../b\n+\t) &&\n+\n+\ttest_path_is_missing a/b/c/tracked &&\n+\t## We would prefer if a/b was still present, though empty, since it\n+\t## was the current working directory\n+\t#test_path_is_dir a/b\n+\t## But the current behavior is that it not only deletes the directory\n+\t## a/b as requested, but also goes and deletes a\n+\ttest_path_is_missing a\n+'\n+\n+test_expect_success 'git version continues working from a deleted dir' '\n+\tmkdir tmp &&\n+\t(\n+\t\tcd tmp &&\n+\t\trm -rf ../tmp &&\n+\t\tgit version\n+\t)\n+'\n+\n+test_submodule_removal () {\n+\tpath_status=$1 &&\n+\tshift &&\n+\n+\ttest_status=\n+\ttest \"$path_status\" = dir && test_status=test_must_fail\n+\n+\t# Actually, while path_status=dir && test_status=test_must_fail\n+\t# reflect our desired behavior, current behavior is:\n+\tpath_status=missing\n+\ttest_status=\n+\n+\ttest_when_finished \"git reset --hard HEAD~1\" &&\n+\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\n+\tgit checkout foo/bar/baz &&\n+\n+\tgit init my_submodule &&\n+\ttouch my_submodule/file &&\n+\tgit -C my_submodule add file &&\n+\tgit -C my_submodule commit -m \"initial commit\" &&\n+\tgit submodule add ./my_submodule &&\n+\tgit commit -m \"Add the submodule\" &&\n+\n+\t(\n+\t\tcd my_submodule &&\n+\t\t$test_status \"$@\"\n+\t) &&\n+\n+\ttest_path_is_${path_status} my_submodule\n+}\n+\n+test_expect_success 'rm -r with -C leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir git -C .. rm -r my_submodule/\n+'\n+\n+test_expect_success 'rm -r leaves submodule if cwd inside' '\n+\ttest_submodule_removal dir \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -r ../my_submodule/\n+'\n+\n+test_expect_success 'rm -rf removes submodule even if cwd inside' '\n+\ttest_submodule_removal missing \\\n+\t\tgit --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"443571","messageId":"cd1f564c4c0df9d6546e35a475e1f95d4afd5f59.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 02/11] setup: introduce startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:26Z","receivedAt":"2021-12-09T05:08:47Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nRemoving the current working directory causes all subsequent git\ncommands run from that directory to get confused and fail with a message\nabout being unable to read the current working directory:\n\n    $ git status\n    fatal: Unable to read current working directory: No such file or directory\n\nNon-git commands likely have similar warnings or even errors, e.g.\n\n    $ bash -c 'echo hello'\n    shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory\n    hello\n\nThis confuses end users, particularly since the command they get the\nerror from is not the one that caused the problem; the problem came from\nthe side-effect of some previous command.\n\nWe would like to avoid removing the current working directory of our\nparent process; towards this end, introduce a new variable,\nstartup_info->original_cwd, that tracks the current working directory\nthat we inherited from our parent process.  For convenience of later\ncomparisons, we prefer that this new variable store a path relative to\nthe toplevel working directory (thus much like 'prefix'), except without\nthe trailing slash.\n\nSubsequent commits will make use of this new variable.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n cache.h       |  2 ++\n common-main.c |  4 ++++\n setup.c       | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex eba12487b99..92e181ea759 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1834,8 +1834,10 @@ void overlay_tree_on_index(struct index_state *istate,\n struct startup_info {\n \tint have_repository;\n \tconst char *prefix;\n+\tconst char *original_cwd;\n };\n extern struct startup_info *startup_info;\n+extern const char *tmp_original_cwd;\n \n /* merge.c */\n struct commit_list;\ndiff --git a/common-main.c b/common-main.c\nindex 71e21dd20a3..aa8d5aba5bb 100644\n--- a/common-main.c\n+++ b/common-main.c\n@@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void)\n int main(int argc, const char **argv)\n {\n \tint result;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \n \ttrace2_initialize_clock();\n \n@@ -49,6 +50,9 @@ int main(int argc, const char **argv)\n \ttrace2_cmd_start(argv);\n \ttrace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP);\n \n+\tif (!strbuf_getcwd(&tmp))\n+\t\ttmp_original_cwd = strbuf_detach(&tmp, NULL);\n+\n \tresult = cmd_main(argc, argv);\n \n \ttrace2_cmd_exit(result);\ndiff --git a/setup.c b/setup.c\nindex 347d7181ae9..af3b8c09abe 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -12,6 +12,7 @@ static int work_tree_config_is_bogus;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n+const char *tmp_original_cwd;\n \n /*\n  * The input parameter must contain an absolute path, and it must already be\n@@ -432,6 +433,69 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static void setup_original_cwd(void)\n+{\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tconst char *worktree = NULL;\n+\tint offset = -1;\n+\n+\tif (!tmp_original_cwd)\n+\t\treturn;\n+\n+\t/*\n+\t * startup_info->original_cwd points to the current working\n+\t * directory we inherited from our parent process, which is a\n+\t * directory we want to avoid removing.\n+\t *\n+\t * For convience, we would like to have the path relative to the\n+\t * worktree instead of an absolute path.\n+\t *\n+\t * Yes, startup_info->original_cwd is usually the same as 'prefix',\n+\t * but differs in two ways:\n+\t *   - prefix has a trailing '/'\n+\t *   - if the user passes '-C' to git, that modifies the prefix but\n+\t *     not startup_info->original_cwd.\n+\t */\n+\n+\t/* Normalize the directory */\n+\tstrbuf_realpath(&tmp, tmp_original_cwd, 1);\n+\tfree((char*)tmp_original_cwd);\n+\ttmp_original_cwd = NULL;\n+\tstartup_info->original_cwd = strbuf_detach(&tmp, NULL);\n+\n+\t/*\n+\t * Get our worktree; we only protect the current working directory\n+\t * if it's in the worktree.\n+\t */\n+\tworktree = get_git_work_tree();\n+\tif (!worktree)\n+\t\tgoto no_prevention_needed;\n+\n+\toffset = dir_inside_of(startup_info->original_cwd, worktree);\n+\tif (offset >= 0) {\n+\t\t/*\n+\t\t * If startup_info->original_cwd == worktree, that is already\n+\t\t * protected and we don't need original_cwd as a secondary\n+\t\t * protection measure.\n+\t\t */\n+\t\tif (!*(startup_info->original_cwd + offset))\n+\t\t\tgoto no_prevention_needed;\n+\n+\t\t/*\n+\t\t * original_cwd was inside worktree; precompose it just as\n+\t\t * we do prefix so that built up paths will match\n+\t\t */\n+\t\tstartup_info->original_cwd = \\\n+\t\t\tprecompose_string_if_needed(startup_info->original_cwd\n+\t\t\t\t\t\t    + offset);\n+\t\treturn;\n+\t}\n+\n+no_prevention_needed:\n+\tfree((char*)startup_info->original_cwd);\n+\tstartup_info->original_cwd = NULL;\n+}\n+\n static int read_worktree_config(const char *var, const char *value, void *vdata)\n {\n \tstruct repository_format *data = vdata;\n@@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n+\tsetup_original_cwd();\n \n \tstrbuf_release(&dir);\n \tstrbuf_release(&gitdir);\n-- \ngitgitgadget\n\n"},{"id":"443572","messageId":"4a1f62b662c7cc1429d67bef3b6b3db02427db0b.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 03/11] unpack-trees: refuse to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:27Z","receivedAt":"2021-12-09T05:08:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nIn the past, when a directory needs to be removed to make room for a\nfile, we have always errored out when that directory contains any\nuntracked (but not ignored) files.  Add an extra condition on that: also\nerror out if the directory is the current working directory we inherited\nfrom our parent process.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 20 +++++++-------------\n unpack-trees.c       | 17 +++++++++++++----\n unpack-trees.h       |  1 +\n 3 files changed, 21 insertions(+), 17 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex a05abd18187..398908dfc93 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -113,7 +113,7 @@ test_expect_success 'checkout does not clean cwd incidentally' '\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git checkout fd_conflict\n+\ttest_required_dir_removal success git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n@@ -144,23 +144,17 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We would rather this failed, but we test for existing\n-\t\t# rather than desired behavior\n-\t\tgit merge fd_conflict 2>../error\n+\t\ttest_must_fail git merge fd_conflict 2>../error\n \t) &&\n \n-\t## Here is the behavior we would rather have:\n-\t#test_path_is_dir dirORfile &&\n-\t#grep \"Refusing to remove the current working directory\" error\n-\t## But instead we test for existing behavior\n-\ttest_path_is_file dirORfile &&\n-\ttest_must_be_empty error\n+\ttest_path_is_dir dirORfile &&\n+\tgrep \"Refusing to remove the current working directory\" error\n '\n \n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git merge fd_conflict\n+\ttest_required_dir_removal success git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n@@ -168,7 +162,7 @@ test_expect_success 'cherry-pick does not clean cwd incidentally' '\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git cherry-pick fd_conflict\n+\ttest_required_dir_removal success git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n@@ -184,7 +178,7 @@ test_expect_success 'revert does not clean cwd incidentally' '\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git revert undo_fd_conflict\n+\ttest_required_dir_removal success git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 89ca95ce90b..6bc16f3a714 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = {\n \t/* ERROR_NOT_UPTODATE_DIR */\n \t\"Updating '%s' would lose untracked files in it\",\n \n+\t/* ERROR_CWD_IN_THE_WAY */\n+\t\"Refusing to remove '%s' since it is the current working directory.\",\n+\n \t/* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */\n \t\"Untracked working tree file '%s' would be overwritten by merge.\",\n \n@@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,\n \tmsgs[ERROR_NOT_UPTODATE_DIR] =\n \t\t_(\"Updating the following directories would lose untracked files in them:\\n%s\");\n \n+\tmsgs[ERROR_CWD_IN_THE_WAY] =\n+\t\t_(\"Refusing to remove the current working directory:\\n%s\");\n+\n \tif (!strcmp(cmd, \"checkout\"))\n \t\tmsg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)\n \t\t      ? _(\"The following untracked working tree files would be removed by checkout:\\n%%s\"\n@@ -2146,10 +2152,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \t\tcnt++;\n \t}\n \n-\t/*\n-\t * Then we need to make sure that we do not lose a locally\n-\t * present file that is not ignored.\n-\t */\n+\t/* Do not lose a locally present file that is not ignored. */\n \tpathbuf = xstrfmt(\"%.*s/\", namelen, ce->name);\n \n \tmemset(&d, 0, sizeof(d));\n@@ -2160,6 +2163,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,\n \tfree(pathbuf);\n \tif (i)\n \t\treturn add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);\n+\n+\t/* Do not lose startup_info->original_cwd */\n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name);\n+\n \treturn cnt;\n }\n \ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 71ffb7eeb0c..efb9edfbb27 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -19,6 +19,7 @@ enum unpack_trees_error_types {\n \tERROR_WOULD_OVERWRITE = 0,\n \tERROR_NOT_UPTODATE_FILE,\n \tERROR_NOT_UPTODATE_DIR,\n+\tERROR_CWD_IN_THE_WAY,\n \tERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN,\n \tERROR_WOULD_LOSE_UNTRACKED_REMOVED,\n \tERROR_BIND_OVERLAP,\n-- \ngitgitgadget\n\n"},{"id":"443573","messageId":"03f2b0b8fb77c1003da5d4f40bdc467c311e0dce.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 04/11] unpack-trees: add special cwd handling","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:28Z","receivedAt":"2021-12-09T05:08:51Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWhen running commands such as `git reset --hard` from a subdirectory, if\nthat subdirectory is in the way of adding needed files, bail with an\nerror message.\n\nNote that this change looks kind of like it duplicates the new lines of\ncode from the previous commit in verify_clean_subdirectory().  However,\nwhen we are preserving untracked files, we would rather any error\nmessages about untracked files being in the way take precedence over\nerror messages about a subdirectory that happens to be the_original_cwd\nbeing in the way.  But in the UNPACK_RESET_OVERWRITE_UNTRACKED case,\nthere is no untracked checking to be done, so we simply add a special\ncase near the top of verify_absent_1.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh |  2 +-\n unpack-trees.c       | 13 +++++++++++--\n 2 files changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 398908dfc93..5af1fec6fec 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -121,7 +121,7 @@ test_expect_success 'reset --hard does not clean cwd incidentally' '\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git reset --hard fd_conflict\n+\ttest_required_dir_removal success git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 6bc16f3a714..5852807d2fb 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2261,10 +2261,19 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tint len;\n \tstruct stat st;\n \n-\tif (o->index_only || !o->update ||\n-\t    o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)\n+\tif (o->index_only || !o->update)\n \t\treturn 0;\n \n+\tif (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) {\n+\t\t/* Avoid nuking startup_info->original_cwd... */\n+\t\tif (startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, ce->name))\n+\t\t\treturn add_rejected_path(o, ERROR_CWD_IN_THE_WAY,\n+\t\t\t\t\t\t ce->name);\n+\t\t/* ...but nuke anything else. */\n+\t\treturn 0;\n+\t}\n+\n \tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"443574","messageId":"d4f8784a6e4e7d9439660871d5d41fd4fe54664c.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 06/11] clean: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:30Z","receivedAt":"2021-12-09T05:08:52Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clean.c      | 44 +++++++++++++++++++++++++++++++++++---------\n t/t2501-cwd-empty.sh |  5 +++--\n 2 files changed, 38 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 98a2860409b..3ff02bbbffe 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_(\"Skipping repository %s\\n\");\n static const char *msg_would_skip_git_dir = N_(\"Would skip repository %s\\n\");\n static const char *msg_warn_remove_failed = N_(\"failed to remove %s\");\n static const char *msg_warn_lstat_failed = N_(\"could not lstat %s\\n\");\n+static const char *msg_skip_cwd = N_(\"Refusing to remove current working directory\\n\");\n+static const char *msg_would_skip_cwd = N_(\"Would refuse to remove current working directory\\n\");\n \n enum color_clean {\n \tCLEAN_COLOR_RESET = 0,\n@@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n {\n \tDIR *dir;\n \tstruct strbuf quoted = STRBUF_INIT;\n+\tstruct strbuf realpath = STRBUF_INIT;\n+\tstruct strbuf real_ocwd = STRBUF_INIT;\n \tstruct dirent *e;\n \tint res = 0, ret = 0, gone = 1, original_len = path->len, len;\n \tstruct string_list dels = STRING_LIST_INIT_DUP;\n@@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \tstrbuf_setlen(path, original_len);\n \n \tif (*dir_gone) {\n-\t\tres = dry_run ? 0 : rmdir(path->buf);\n-\t\tif (!res)\n-\t\t\t*dir_gone = 1;\n-\t\telse {\n-\t\t\tint saved_errno = errno;\n-\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n-\t\t\terrno = saved_errno;\n-\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t/*\n+\t\t * Normalize path components in path->buf, e.g. change '\\' to\n+\t\t * '/' on Windows.\n+\t\t */\n+\t\tstrbuf_realpath(&realpath, path->buf, 1);\n+\n+\t\t/*\n+\t\t * path and realpath are absolute; for comparison, we would\n+\t\t * like to transform startup_info->original_cwd to an absolute\n+\t\t * path too.\n+\t\t */\n+\t\t if (startup_info->original_cwd)\n+\t\t\t strbuf_realpath(&real_ocwd,\n+\t\t\t\t\t startup_info->original_cwd, 1);\n+\n+\t\tif (!strbuf_cmp(&realpath, &real_ocwd)) {\n+\t\t\tprintf(\"%s\", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd));\n \t\t\t*dir_gone = 0;\n-\t\t\tret = 1;\n+\t\t} else {\n+\t\t\tres = dry_run ? 0 : rmdir(path->buf);\n+\t\t\tif (!res)\n+\t\t\t\t*dir_gone = 1;\n+\t\t\telse {\n+\t\t\t\tint saved_errno = errno;\n+\t\t\t\tquote_path(path->buf, prefix, &quoted, 0);\n+\t\t\t\terrno = saved_errno;\n+\t\t\t\twarning_errno(_(msg_warn_remove_failed), quoted.buf);\n+\t\t\t\t*dir_gone = 0;\n+\t\t\t\tret = 1;\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,\n \t\t\tprintf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);\n \t}\n out:\n+\tstrbuf_release(&realpath);\n+\tstrbuf_release(&real_ocwd);\n \tstrbuf_release(&quoted);\n \tstring_list_clear(&dels, 0);\n \treturn ret;\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex e4502d24d57..b1182390ba3 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -230,8 +230,9 @@ test_incidental_untracked_dir_removal () {\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n-\t\tgit -C .. clean -fd -e warnings . >warnings\n+\ttest_incidental_untracked_dir_removal success \\\n+\t\tgit -C .. clean -fd -e warnings . >warnings &&\n+\tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"443575","messageId":"e75f6c3f9ad3a808a7013f7ece3e2966df641381.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 05/11] symlinks: do not include startup_info->original_cwd in dir removal","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:29Z","receivedAt":"2021-12-09T05:08:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nsymlinks has a pair of schedule_dir_for_removal() and\nremove_scheduled_dirs() functions that ensure that directories made\nempty by removing other files also themselves get removed.  However, we\nwant to exclude startup_info->original_cwd and leave it around.  This\navoids the user getting confused by subsequent git commands (and non-git\ncommands) that would otherwise report confusing messages about being\nunable to read the current working directory.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n symlinks.c           |  8 +++++++-\n t/t2501-cwd-empty.sh | 10 +++++-----\n 2 files changed, 12 insertions(+), 6 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 5232d02020c..c667baa949b 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len)\n {\n \twhile (removal.len > new_len) {\n \t\tremoval.buf[removal.len] = '\\0';\n-\t\tif (rmdir(removal.buf))\n+\t\tif ((startup_info->original_cwd &&\n+\t\t     !strcmp(removal.buf, startup_info->original_cwd)) ||\n+\t\t    rmdir(removal.buf))\n \t\t\tbreak;\n \t\tdo {\n \t\t\tremoval.len--;\n@@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len)\n {\n \tint match_len, last_slash, i, previous_slash;\n \n+\tif (startup_info->original_cwd &&\n+\t    !strcmp(name, startup_info->original_cwd))\n+\t\treturn;\t/* Do not remove the current working directory */\n+\n \tmatch_len = last_slash = i =\n \t\tlongest_path_match(name, len, removal.buf, removal.len,\n \t\t\t\t   &previous_slash);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 5af1fec6fec..e4502d24d57 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -109,7 +109,7 @@ test_required_dir_removal () {\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git checkout init\n+\ttest_incidental_dir_removal success git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n@@ -117,7 +117,7 @@ test_expect_success 'checkout fails if cwd needs to be removed' '\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git reset --hard init\n+\ttest_incidental_dir_removal success git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n@@ -125,7 +125,7 @@ test_expect_success 'reset --hard fails if cwd needs to be removed' '\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git merge reverted\n+\ttest_incidental_dir_removal success git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -158,7 +158,7 @@ test_expect_success 'merge fails if cwd needs to be removed' '\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git cherry-pick reverted\n+\ttest_incidental_dir_removal success git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n@@ -174,7 +174,7 @@ test_expect_success 'rebase fails if cwd needs to be removed' '\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git revert HEAD\n+\ttest_incidental_dir_removal success git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-- \ngitgitgadget\n\n"},{"id":"443576","messageId":"fd500cc1843826157c79d2823ed26acce40525b0.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 07/11] rebase: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:31Z","receivedAt":"2021-12-09T05:08:55Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince rebase spawns a `checkout` subprocess, make sure we run that from\nthe startup_info->original_cwd directory, so that the checkout process\nknows to protect that directory.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n sequencer.c          | 2 ++\n t/t2501-cwd-empty.sh | 4 ++--\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex ea96837cde3..83f257e7fa4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n \n \tcmd.git_cmd = 1;\n \n+\tif (startup_info->original_cwd)\n+\t\tcmd.dir = startup_info->original_cwd;\n \tstrvec_push(&cmd.args, \"checkout\");\n \tstrvec_push(&cmd.args, commit);\n \tstrvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex b1182390ba3..52335a8afe9 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rebase reverted\n+\ttest_incidental_dir_removal success git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal failure git rebase fd_conflict\n+\ttest_required_dir_removal success git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-- \ngitgitgadget\n\n"},{"id":"443577","messageId":"6f3c4fd49b9e816ff45971f26330ab4fba959b80.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 08/11] stash: do not attempt to remove startup_info->original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:32Z","receivedAt":"2021-12-09T05:08:57Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nSince stash spawns a `clean` subprocess, make sure we run that from the\nstartup_info->original_cwd directory, so that the `clean` processs knows\nto protect that directory.  Also, since the `clean` command might no\nlonger run from the toplevel, pass the ':/' magic pathspec to ensure we\nstill clean from the toplevel.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/stash.c      | 4 +++-\n t/t2501-cwd-empty.sh | 2 +-\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex a0ccc8654df..de0e432a4ff 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1485,8 +1485,10 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \t\t\tstruct child_process cp = CHILD_PROCESS_INIT;\n \n \t\t\tcp.git_cmd = 1;\n+\t\t\tif (startup_info->original_cwd)\n+\t\t\t\tcp.dir = startup_info->original_cwd;\n \t\t\tstrvec_pushl(&cp.args, \"clean\", \"--force\",\n-\t\t\t\t     \"--quiet\", \"-d\", NULL);\n+\t\t\t\t     \"--quiet\", \"-d\", \":/\", NULL);\n \t\t\tif (include_untracked == INCLUDE_ALL_FILES)\n \t\t\t\tstrvec_push(&cp.args, \"-x\");\n \t\t\tif (run_command(&cp)) {\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex 52335a8afe9..be9ef903bd4 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -236,7 +236,7 @@ test_expect_success 'clean does not remove cwd incidentally' '\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal failure \\\n+\ttest_incidental_untracked_dir_removal success \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n\n"},{"id":"443578","messageId":"1a66b1a238688c091124434865580d00317bf491.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 09/11] dir: avoid incidentally removing the original_cwd in remove_path()","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:33Z","receivedAt":"2021-12-09T05:08:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nModern git often tries to avoid leaving empty directories around when\nremoving files.  Originally, it did not bother.  This behavior started\nwith commit 80e21a9ed809 (merge-recursive::removeFile: remove empty\ndirectories, 2005-11-19), stating the reason simply as:\n\n    When the last file in a directory is removed as the result of a\n    merge, try to rmdir the now-empty directory.\n\nThis was reimplemented in C and renamed to remove_path() in commit\ne1b3a2cad7 (\"Build-in merge-recursive\", 2008-02-07), but was still\ninternal to merge-recursive.\n\nThis trend towards removing leading empty directories continued with\ncommit d9b814cc97f1 (Add builtin \"git rm\" command, 2006-05-19), which\nstated the reasoning as:\n\n    The other question is what to do with leading directories. The old\n    \"git rm\" script didn't do anything, which is somewhat inconsistent.\n    This one will actually clean up directories that have become empty\n    as a result of removing the last file, but maybe we want to have a\n    flag to decide the behaviour?\n\nremove_path() in dir.c was added in 4a92d1bfb784 (Add remove_path: a\nfunction to remove as much as possible of a path, 2008-09-27), because\nit was noted that we had two separate implementations of the same idea\nAND both were buggy.  It described the purpose of the function as\n\n    a function to remove as much as possible of a path\n\nWhy remove as much as possible?  Well, at the time we probably would\nhave said something like:\n\n  * removing leading directories makes things feel tidy\n  * removing leading directories doesn't hurt anything so long as they\n    had no files in them.\n\nBut I don't believe those reasons hold when the empty directory happens\nto be the current working directory we inherited from our parent\nprocess.  Leaving the parent process in a deleted directory can cause\nuser confusion when subsequent processes fail: any git command, for\nexample, will immediately fail with\n\n    fatal: Unable to read current working directory: No such file or directory\n\nOther commands may similarly get confused.  Modify remove_path() so that\nthe empty leading directories it also deletes does not include the\ncurrent working directory we inherited from our parent process.  I have\nlooked through every caller of remove_path() in the current codebase to\nmake sure that all should take this change.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n dir.c                |  3 +++\n dir.h                |  6 +++++-\n t/t2501-cwd-empty.sh | 12 ++++--------\n 3 files changed, 12 insertions(+), 9 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 94489298f4c..97d6b71c872 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3327,6 +3327,9 @@ int remove_path(const char *name)\n \t\tslash = dirs + (slash - name);\n \t\tdo {\n \t\t\t*slash = '\\0';\n+\t\t\tif (startup_info->original_cwd &&\n+\t\t\t    !strcmp(startup_info->original_cwd, dirs))\n+\t\t\t\tbreak;\n \t\t} while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/')));\n \t\tfree(dirs);\n \t}\ndiff --git a/dir.h b/dir.h\nindex 83f46c0fb4c..d6a5d03bec2 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -504,7 +504,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n  */\n int remove_dir_recursively(struct strbuf *path, int flag);\n \n-/* tries to remove the path with empty directories along it, ignores ENOENT */\n+/*\n+ * Tries to remove the path, along with leading empty directories so long as\n+ * those empty directories are not startup_info->original_cwd.  Ignores\n+ * ENOENT.\n+ */\n int remove_path(const char *path);\n \n int fspathcmp(const char *a, const char *b);\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9ef903bd4..ce2efb9d30a 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -182,12 +182,12 @@ test_expect_success 'revert fails if cwd needs to be removed' '\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal failure git rm bar/baz.t\n+\ttest_incidental_dir_removal success git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal failure git apply ../patch\n+\ttest_incidental_dir_removal success git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n@@ -271,12 +271,8 @@ test_expect_success '`rm -rf dir` even with only tracked files will remove somet\n \t) &&\n \n \ttest_path_is_missing a/b/c/tracked &&\n-\t## We would prefer if a/b was still present, though empty, since it\n-\t## was the current working directory\n-\t#test_path_is_dir a/b\n-\t## But the current behavior is that it not only deletes the directory\n-\t## a/b as requested, but also goes and deletes a\n-\ttest_path_is_missing a\n+\ttest_path_is_missing a/b/c &&\n+\ttest_path_is_dir a/b\n '\n \n test_expect_success 'git version continues working from a deleted dir' '\n-- \ngitgitgadget\n\n"},{"id":"443579","messageId":"4a18efd51ef65ee6e4d06ac9dfed5b1645609549.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 10/11] dir: new flag to remove_dir_recurse() to spare the original_cwd","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:34Z","receivedAt":"2021-12-09T05:09:00Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nremove_dir_recurse(), and its non-static wrapper called\nremove_dir_recursively(), both take flags for modifying its behavior.\nAs with the previous commits, we would generally like to protect\nthe original_cwd, but we want to forced user commands (e.g. 'git rm -rf\n...') or other special cases to remove it.  Add a flag for this purpose.\nAfter reading through every caller of remove_dir_recursively() in the\ncurrent codebase, there was only one that should be adjusted and that\none only in a very unusual circumstance.  Add a pair of new testcases to\nhighlight that very specific case involving submodules && --git-dir &&\n--work-tree.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/rm.c         |  3 ++-\n dir.c                | 12 +++++++++---\n dir.h                |  3 +++\n t/t2501-cwd-empty.sh |  5 -----\n 4 files changed, 14 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/rm.c b/builtin/rm.c\nindex 3d0967cdc11..b4132e5d8ee 100644\n--- a/builtin/rm.c\n+++ b/builtin/rm.c\n@@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix)\n \tif (!index_only) {\n \t\tint removed = 0, gitmodules_modified = 0;\n \t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tint flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0;\n \t\tfor (i = 0; i < list.nr; i++) {\n \t\t\tconst char *path = list.entry[i].name;\n \t\t\tif (list.entry[i].is_submodule) {\n \t\t\t\tstrbuf_reset(&buf);\n \t\t\t\tstrbuf_addstr(&buf, path);\n-\t\t\t\tif (remove_dir_recursively(&buf, 0))\n+\t\t\t\tif (remove_dir_recursively(&buf, flag))\n \t\t\t\t\tdie(_(\"could not remove '%s'\"), path);\n \n \t\t\t\tremoved = 1;\ndiff --git a/dir.c b/dir.c\nindex 97d6b71c872..52064345a6b 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3204,6 +3204,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tint ret = 0, original_len = path->len, len, kept_down = 0;\n \tint only_empty = (flag & REMOVE_DIR_EMPTY_ONLY);\n \tint keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL);\n+\tint purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD);\n \tstruct object_id submodule_head;\n \n \tif ((flag & REMOVE_DIR_KEEP_NESTED_GIT) &&\n@@ -3259,9 +3260,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)\n \tclosedir(dir);\n \n \tstrbuf_setlen(path, original_len);\n-\tif (!ret && !keep_toplevel && !kept_down)\n-\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n-\telse if (kept_up)\n+\tif (!ret && !keep_toplevel && !kept_down) {\n+\t\tif (!purge_original_cwd &&\n+\t\t    startup_info->original_cwd &&\n+\t\t    !strcmp(startup_info->original_cwd, path->buf))\n+\t\t\tret = -1; /* Do not remove current working directory */\n+\t\telse\n+\t\t\tret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;\n+\t} else if (kept_up)\n \t\t/*\n \t\t * report the uplevel that it is not an error that we\n \t\t * did not rmdir() our directory.\ndiff --git a/dir.h b/dir.h\nindex d6a5d03bec2..8e02dfb505d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);\n /* Remove the contents of path, but leave path itself. */\n #define REMOVE_DIR_KEEP_TOPLEVEL 04\n \n+/* Remove the_original_cwd too */\n+#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08\n+\n /*\n  * Remove path and its contents, recursively. flags is a combination\n  * of the above REMOVE_DIR_* constants. Return 0 on success.\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex ce2efb9d30a..bc92230f2f2 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -291,11 +291,6 @@ test_submodule_removal () {\n \ttest_status=\n \ttest \"$path_status\" = dir && test_status=test_must_fail\n \n-\t# Actually, while path_status=dir && test_status=test_must_fail\n-\t# reflect our desired behavior, current behavior is:\n-\tpath_status=missing\n-\ttest_status=\n-\n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n \ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n \n-- \ngitgitgadget\n\n"},{"id":"443580","messageId":"63781ed8b8e9a6bfb7326c04f3338c45a0dd5f1f.1639026515.git.gitgitgadget@gmail.com","threadId":"56952","inReplyTo":"pull.1140.v6.git.git.1639026515.gitgitgadget@gmail.com","subject":"[PATCH v6 11/11] t2501: simplify the tests since we can now assume desired behavior","fromName":"Elijah Newren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-12-09T05:08:35Z","receivedAt":"2021-12-09T05:09:02Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe no longer are dealing with a mixture of previous and desired\nbehavior, so simplify the tests a bit.\n\nAcked-by: Derrick Stolee <stolee@gmail.com>\nAcked-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t2501-cwd-empty.sh | 123 +++++++++++++------------------------------\n 1 file changed, 36 insertions(+), 87 deletions(-)\n\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex bc92230f2f2..f6d8d7d03d7 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -32,9 +32,6 @@ test_expect_success setup '\n '\n \n test_incidental_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -44,88 +41,57 @@ test_incidental_dir_removal () {\n \t\tcd foo &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure foo still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing foo/bar/baz &&\n \ttest_path_is_missing foo/bar &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir foo\n-\telse\n-\t\ttest_path_is_missing foo\n-\tfi\n+\ttest_path_is_dir foo\n }\n \n test_required_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \tgit checkout df_conflict^{commit} &&\n \ttest_when_finished \"git clean -fdx\" &&\n \n \t(\n \t\tcd dirORfile &&\n \n-\t\t# We'd like for the command to fail (much as it would if there\n-\t\t# was an untracked file there), and for the index and worktree\n-\t\t# to be left clean with pwd and git status working afterwards.\n-\t\t# But test for existing rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest_must_fail \"$@\" 2>../error &&\n-\t\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n-\n-\t\t\tgit diff --exit-code HEAD &&\n-\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t\"$@\" &&\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Ensure command refuses to run\n+\t\ttest_must_fail \"$@\" 2>../error &&\n+\t\tgrep \"Refusing to remove.*current working directory\" ../error &&\n+\n+\t\t# ...and that the index and working tree are left clean\n+\t\tgit diff --exit-code HEAD &&\n+\n+\t\t# Ensure that getcwd and git status do not error out (which\n+\t\t# they might if the current working directory had been removed)\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \n-\t# Although we want dirORfile to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir dirORfile\n-\telse\n-\t\ttest_path_is_file dirORfile\n-\tfi\n+\ttest_path_is_dir dirORfile\n }\n \n test_expect_success 'checkout does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git checkout init\n+\ttest_incidental_dir_removal git checkout init\n '\n \n test_expect_success 'checkout fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git checkout fd_conflict\n+\ttest_required_dir_removal git checkout fd_conflict\n '\n \n test_expect_success 'reset --hard does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git reset --hard init\n+\ttest_incidental_dir_removal git reset --hard init\n '\n \n test_expect_success 'reset --hard fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git reset --hard fd_conflict\n+\ttest_required_dir_removal git reset --hard fd_conflict\n '\n \n test_expect_success 'merge does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git merge reverted\n+\ttest_incidental_dir_removal git merge reverted\n '\n \n # This file uses some simple merges where\n@@ -154,46 +120,43 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n GIT_TEST_MERGE_ALGORITHM=ort\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git merge fd_conflict\n+\ttest_required_dir_removal git merge fd_conflict\n '\n \n test_expect_success 'cherry-pick does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git cherry-pick reverted\n+\ttest_incidental_dir_removal git cherry-pick reverted\n '\n \n test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git cherry-pick fd_conflict\n+\ttest_required_dir_removal git cherry-pick fd_conflict\n '\n \n test_expect_success 'rebase does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rebase reverted\n+\ttest_incidental_dir_removal git rebase reverted\n '\n \n test_expect_success 'rebase fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git rebase fd_conflict\n+\ttest_required_dir_removal git rebase fd_conflict\n '\n \n test_expect_success 'revert does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git revert HEAD\n+\ttest_incidental_dir_removal git revert HEAD\n '\n \n test_expect_success 'revert fails if cwd needs to be removed' '\n-\ttest_required_dir_removal success git revert undo_fd_conflict\n+\ttest_required_dir_removal git revert undo_fd_conflict\n '\n \n test_expect_success 'rm does not clean cwd incidentally' '\n-\ttest_incidental_dir_removal success git rm bar/baz.t\n+\ttest_incidental_dir_removal git rm bar/baz.t\n '\n \n test_expect_success 'apply does not remove cwd incidentally' '\n \tgit diff HEAD HEAD~1 >patch &&\n-\ttest_incidental_dir_removal success git apply ../patch\n+\ttest_incidental_dir_removal git apply ../patch\n '\n \n test_incidental_untracked_dir_removal () {\n-\tworks=$1 &&\n-\tshift &&\n-\n \ttest_when_finished \"git reset --hard\" &&\n \n \tgit checkout foo/bar/baz^{commit} &&\n@@ -205,38 +168,24 @@ test_incidental_untracked_dir_removal () {\n \t\tcd untracked &&\n \t\t\"$@\" &&\n \n-\t\t# Although we want pwd & git status to pass, test for existing\n-\t\t# rather than desired behavior.\n-\t\tif test \"$works\" = \"success\"\n-\t\tthen\n-\t\t\ttest-tool getcwd &&\n-\t\t\tgit status --porcelain\n-\t\telse\n-\t\t\t! test-tool getcwd &&\n-\t\t\ttest_might_fail git status --porcelain\n-\t\tfi\n+\t\t# Make sure untracked still exists, and commands needing it work\n+\t\ttest-tool getcwd &&\n+\t\tgit status --porcelain\n \t) &&\n \ttest_path_is_missing empty &&\n \ttest_path_is_missing untracked/random &&\n \n-\t# Although we want dir to be present, test for existing rather\n-\t# than desired behavior.\n-\tif test \"$works\" = \"success\"\n-\tthen\n-\t\ttest_path_is_dir untracked\n-\telse\n-\t\ttest_path_is_missing untracked\n-\tfi\n+\ttest_path_is_dir untracked\n }\n \n test_expect_success 'clean does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n \tgrep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\n-\ttest_incidental_untracked_dir_removal success \\\n+\ttest_incidental_untracked_dir_removal \\\n \t\tgit stash --include-untracked\n '\n \n-- \ngitgitgadget\n"},{"id":"446870","messageId":"kl6lilu71rzl.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"f8efb7446c33f14631b088ac043aca8a403a3250.1638340854.git.gitgitgadget@gmail.com","subject":"[Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-01-25T20:26:54Z","receivedAt":"2022-01-25T20:27:07Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Elijah Newren <newren@gmail.com>\n>\n> Since rebase spawns a `checkout` subprocess, make sure we run that from\n> the startup_info->original_cwd directory, so that the checkout process\n> knows to protect that directory.\n>\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n>  sequencer.c          | 2 ++\n>  t/t2501-cwd-empty.sh | 4 ++--\n>  2 files changed, 4 insertions(+), 2 deletions(-)\n>\n> diff --git a/sequencer.c b/sequencer.c\n> index ea96837cde3..83f257e7fa4 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n>  \n>  \tcmd.git_cmd = 1;\n>  \n> +\tif (startup_info->original_cwd)\n> +\t\tcmd.dir = startup_info->original_cwd;\n>  \tstrvec_push(&cmd.args, \"checkout\");\n>  \tstrvec_push(&cmd.args, commit);\n>  \tstrvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\n> diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n> index b1182390ba3..52335a8afe9 100755\n> --- a/t/t2501-cwd-empty.sh\n> +++ b/t/t2501-cwd-empty.sh\n> @@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>  '\n>  \n>  test_expect_success 'rebase does not clean cwd incidentally' '\n> -\ttest_incidental_dir_removal failure git rebase reverted\n> +\ttest_incidental_dir_removal success git rebase reverted\n>  '\n>  \n>  test_expect_success 'rebase fails if cwd needs to be removed' '\n> -\ttest_required_dir_removal failure git rebase fd_conflict\n> +\ttest_required_dir_removal success git rebase fd_conflict\n>  '\n>  \n>  test_expect_success 'revert does not clean cwd incidentally' '\n> -- \n> gitgitgadget\n\nThis commit (which is already in master) introduces a bug that breaks\nrebase when rebasing inside a subdirectory of a worktree. You can see\nthat the below test fails with:\n\n  error: The following untracked working tree files would be overwritten by merge:\n          a/b/c\n  Please move or remove them before you merge.\n\nThis only affects subdirectories in worktrees, i.e. rebasing anywhere in\nthe `main-wt` directory is fine, and rebasing from the top of `other-wt`\nis fine, but `other-wt/any/other/dir` fails.\n\nI haven't tracked down the root cause yet, but judging from the commit,\nI would suppose that the checkout is being spawned in the wrong\ndirectory, causing the files to not be cleaned up.\n\n---\n t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++\n 1 file changed, 29 insertions(+)\n\ndiff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh\nindex 23dbd3c82e..8b8b66538b 100755\n--- a/t/t3400-rebase.sh\n+++ b/t/t3400-rebase.sh\n@@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'\n \tmv actual_logs .git/logs\n '\n \n+test_expect_success 'rebase when inside worktree subdirectory' '\n+\tgit init main-wt &&\n+\t(\n+\t\tcd main-wt &&\n+\t\tgit commit --allow-empty -m \"initial\" &&\n+\t\t# create commit with foo/bar/baz\n+\t\tmkdir -p foo/bar &&\n+\t\ttouch foo/bar/baz &&\n+\t\tgit add foo/bar/baz &&\n+\t\tgit commit -m \"add foo/bar/baz\" &&\n+\t\t# create commit with a/b/c\n+\t\tmkdir -p a/b &&\n+\t\ttouch a/b/c &&\n+\t\tgit add a/b/c &&\n+\t\tgit commit -m \"add a/b/c\" &&\n+\t\t# create another branch for our other worktree\n+\t\tgit branch other &&\n+\t\tgit worktree add ../other-wt other &&\n+\t\t(\n+\t\t\tcd ../other-wt &&\n+\t\t\tmkdir -p random/dir &&\n+\t\t\t(\n+\t\t\t\tcd random/dir &&\n+\t\t\t\tgit rebase --onto HEAD^^ HEAD^  # drops the HEAD^ commit\n+\t\t\t)\n+\t\t)\n+\t)\n+'\n+\n test_done\n-- \n2.35.0.rc0.227.g00780c9af4-goog\n"},{"id":"446897","messageId":"CABPp-BFdD=f82QvQfokD346YT6aCQ=WwZ09S-a=BPXXj5_LZkg@mail.gmail.com","threadId":"56952","inReplyTo":"kl6lilu71rzl.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-25T23:59:08Z","receivedAt":"2022-01-25T23:59:24Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n>\n> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> > From: Elijah Newren <newren@gmail.com>\n> >\n> > Since rebase spawns a `checkout` subprocess, make sure we run that from\n> > the startup_info->original_cwd directory, so that the checkout process\n> > knows to protect that directory.\n> >\n> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> > ---\n> >  sequencer.c          | 2 ++\n> >  t/t2501-cwd-empty.sh | 4 ++--\n> >  2 files changed, 4 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/sequencer.c b/sequencer.c\n> > index ea96837cde3..83f257e7fa4 100644\n> > --- a/sequencer.c\n> > +++ b/sequencer.c\n> > @@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n> >\n> >       cmd.git_cmd = 1;\n> >\n> > +     if (startup_info->original_cwd)\n> > +             cmd.dir = startup_info->original_cwd;\n> >       strvec_push(&cmd.args, \"checkout\");\n> >       strvec_push(&cmd.args, commit);\n> >       strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\n> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n> > index b1182390ba3..52335a8afe9 100755\n> > --- a/t/t2501-cwd-empty.sh\n> > +++ b/t/t2501-cwd-empty.sh\n> > @@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n> >  '\n> >\n> >  test_expect_success 'rebase does not clean cwd incidentally' '\n> > -     test_incidental_dir_removal failure git rebase reverted\n> > +     test_incidental_dir_removal success git rebase reverted\n> >  '\n> >\n> >  test_expect_success 'rebase fails if cwd needs to be removed' '\n> > -     test_required_dir_removal failure git rebase fd_conflict\n> > +     test_required_dir_removal success git rebase fd_conflict\n> >  '\n> >\n> >  test_expect_success 'revert does not clean cwd incidentally' '\n> > --\n> > gitgitgadget\n>\n> This commit (which is already in master) introduces a bug that breaks\n> rebase when rebasing inside a subdirectory of a worktree. You can see\n> that the below test fails with:\n>\n>   error: The following untracked working tree files would be overwritten by merge:\n>           a/b/c\n>   Please move or remove them before you merge.\n\nThanks for the detailed report -- with a full testcase!\n\n> This only affects subdirectories in worktrees, i.e. rebasing anywhere in\n> the `main-wt` directory is fine, and rebasing from the top of `other-wt`\n> is fine, but `other-wt/any/other/dir` fails.\n>\n> I haven't tracked down the root cause yet, but judging from the commit,\n> I would suppose that the checkout is being spawned in the wrong\n> directory, causing the files to not be cleaned up.\n\nThere's nothing wrong with running checkout from a subdirectory.  It\nis unfortunate that setup.c auto-discovers both the git directory and\nthe working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\nthe case of a non-main worktree; it's not particularly friendly for\nsubcommands.  Of course, it's also unfortunate that sequencer still\nforks subprocesses other than those requested by a user with e.g.\n--exec.\n\nBut, anyway, I've got a patch that I'll send as soon as it passes CI\n(https://github.com/git/git/pull/1205).\n\n> ---\n>  t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++\n>  1 file changed, 29 insertions(+)\n>\n> diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh\n> index 23dbd3c82e..8b8b66538b 100755\n> --- a/t/t3400-rebase.sh\n> +++ b/t/t3400-rebase.sh\n> @@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'\n>         mv actual_logs .git/logs\n>  '\n>\n> +test_expect_success 'rebase when inside worktree subdirectory' '\n> +       git init main-wt &&\n> +       (\n> +               cd main-wt &&\n> +               git commit --allow-empty -m \"initial\" &&\n> +               # create commit with foo/bar/baz\n> +               mkdir -p foo/bar &&\n> +               touch foo/bar/baz &&\n> +               git add foo/bar/baz &&\n> +               git commit -m \"add foo/bar/baz\" &&\n> +               # create commit with a/b/c\n> +               mkdir -p a/b &&\n> +               touch a/b/c &&\n> +               git add a/b/c &&\n> +               git commit -m \"add a/b/c\" &&\n> +               # create another branch for our other worktree\n> +               git branch other &&\n> +               git worktree add ../other-wt other &&\n> +               (\n> +                       cd ../other-wt &&\n> +                       mkdir -p random/dir &&\n> +                       (\n> +                               cd random/dir &&\n> +                               git rebase --onto HEAD^^ HEAD^  # drops the HEAD^ commit\n> +                       )\n> +               )\n> +       )\n> +'\n> +\n>  test_done\n> --\n> 2.35.0.rc0.227.g00780c9af4-goog\n"},{"id":"446903","messageId":"kl6la6fj1gpb.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"CABPp-BFdD=f82QvQfokD346YT6aCQ=WwZ09S-a=BPXXj5_LZkg@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-01-26T00:30:40Z","receivedAt":"2022-01-26T00:30:45Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n>>\n>> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>\n>> > From: Elijah Newren <newren@gmail.com>\n>> >\n>> > Since rebase spawns a `checkout` subprocess, make sure we run that from\n>> > the startup_info->original_cwd directory, so that the checkout process\n>> > knows to protect that directory.\n>> >\n>> > Signed-off-by: Elijah Newren <newren@gmail.com>\n>> > ---\n>> >  sequencer.c          | 2 ++\n>> >  t/t2501-cwd-empty.sh | 4 ++--\n>> >  2 files changed, 4 insertions(+), 2 deletions(-)\n>> >\n>> > diff --git a/sequencer.c b/sequencer.c\n>> > index ea96837cde3..83f257e7fa4 100644\n>> > --- a/sequencer.c\n>> > +++ b/sequencer.c\n>> > @@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n>> >\n>> >       cmd.git_cmd = 1;\n>> >\n>> > +     if (startup_info->original_cwd)\n>> > +             cmd.dir = startup_info->original_cwd;\n>> >       strvec_push(&cmd.args, \"checkout\");\n>> >       strvec_push(&cmd.args, commit);\n>> >       strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\n>> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n>> > index b1182390ba3..52335a8afe9 100755\n>> > --- a/t/t2501-cwd-empty.sh\n>> > +++ b/t/t2501-cwd-empty.sh\n>> > @@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>> >  '\n>> >\n>> >  test_expect_success 'rebase does not clean cwd incidentally' '\n>> > -     test_incidental_dir_removal failure git rebase reverted\n>> > +     test_incidental_dir_removal success git rebase reverted\n>> >  '\n>> >\n>> >  test_expect_success 'rebase fails if cwd needs to be removed' '\n>> > -     test_required_dir_removal failure git rebase fd_conflict\n>> > +     test_required_dir_removal success git rebase fd_conflict\n>> >  '\n>> >\n>> >  test_expect_success 'revert does not clean cwd incidentally' '\n>> > --\n>> > gitgitgadget\n>>\n>> This commit (which is already in master) introduces a bug that breaks\n>> rebase when rebasing inside a subdirectory of a worktree. You can see\n>> that the below test fails with:\n>>\n>>   error: The following untracked working tree files would be overwritten by merge:\n>>           a/b/c\n>>   Please move or remove them before you merge.\n>\n> Thanks for the detailed report -- with a full testcase!\n\nGlad to be of help :)\n\n>> This only affects subdirectories in worktrees, i.e. rebasing anywhere in\n>> the `main-wt` directory is fine, and rebasing from the top of `other-wt`\n>> is fine, but `other-wt/any/other/dir` fails.\n>>\n>> I haven't tracked down the root cause yet, but judging from the commit,\n>> I would suppose that the checkout is being spawned in the wrong\n>> directory, causing the files to not be cleaned up.\n>\n> There's nothing wrong with running checkout from a subdirectory.  It\n> is unfortunate that setup.c auto-discovers both the git directory and\n> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> the case of a non-main worktree; it's not particularly friendly for\n> subcommands.  Of course, it's also unfortunate that sequencer still\n> forks subprocesses other than those requested by a user with e.g.\n> --exec.\n>\n> But, anyway, I've got a patch that I'll send as soon as it passes CI\n> (https://github.com/git/git/pull/1205).\n\nI wish I had seen this email before diving into debugging it myself\nsince your fix is more comprehensive, but it was a good learning\nexperience nonetheless.\n"},{"id":"446905","messageId":"CAPig+cT3Zqa2BLsZ3ZVPCj4-bW5eb35sWXV-mYLqA-dvzC8XJg@mail.gmail.com","threadId":"56952","inReplyTo":"CABPp-BFdD=f82QvQfokD346YT6aCQ=WwZ09S-a=BPXXj5_LZkg@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-01-26T00:32:27Z","receivedAt":"2022-01-26T00:32:47Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:\n> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n> > This commit (which is already in master) introduces a bug that breaks\n> > rebase when rebasing inside a subdirectory of a worktree. You can see\n> > that the below test fails with:\n>\n> There's nothing wrong with running checkout from a subdirectory.  It\n> is unfortunate that setup.c auto-discovers both the git directory and\n> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> the case of a non-main worktree; it's not particularly friendly for\n> subcommands.  Of course, it's also unfortunate that sequencer still\n> forks subprocesses other than those requested by a user with e.g.\n> --exec.\n>\n> But, anyway, I've got a patch that I'll send as soon as it passes CI\n> (https://github.com/git/git/pull/1205).\n>\n> > +test_expect_success 'rebase when inside worktree subdirectory' '\n> > +       git init main-wt &&\n> > +       (\n> > +               cd main-wt &&\n> > +               git commit --allow-empty -m \"initial\" &&\n> > +               # create commit with foo/bar/baz\n> > +               mkdir -p foo/bar &&\n> > +               touch foo/bar/baz &&\n> > +               git add foo/bar/baz &&\n> > +               git commit -m \"add foo/bar/baz\" &&\n> > +               # create commit with a/b/c\n> > +               mkdir -p a/b &&\n> > +               touch a/b/c &&\n> > +               git add a/b/c &&\n> > +               git commit -m \"add a/b/c\" &&\n> > +               # create another branch for our other worktree\n> > +               git branch other &&\n> > +               git worktree add ../other-wt other &&\n> > +               (\n> > +                       cd ../other-wt &&\n> > +                       mkdir -p random/dir &&\n> > +                       (\n> > +                               cd random/dir &&\n> > +                               git rebase --onto HEAD^^ HEAD^  # drops the HEAD^ commit\n> > +                       )\n> > +               )\n> > +       )\n> > +'\n\nThis is entirely minor, but all the inner subshells in this test are\nsuperfluous. The outermost (...) will ensure that the working\ndirectory is restored regardless of whether anything within its body\nfails, no matter how much you `cd` around. Thus, all the inner\nsubshells could be dropped.\n\nAn alternative would be to close the outermost subshell immediately\nafter `git worktree add`:\n\n    git init main-wt &&\n    (\n        cd main-wt &&\n        ...\n        git worktree add ../other-wt other\n    ) &&\n    mkdir -p other-wt/random/dir &&\n    (\n        cd other-wt/random/dir &&\n        ...\n    )\n\nhowever, that may not buy you any clarity.\n"},{"id":"446906","messageId":"CAPig+cRCzUinKr0iQkDMK0Af4=47xKZJKphcuTRxt4BaxrYwfA@mail.gmail.com","threadId":"56952","inReplyTo":"CAPig+cT3Zqa2BLsZ3ZVPCj4-bW5eb35sWXV-mYLqA-dvzC8XJg@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-01-26T00:38:56Z","receivedAt":"2022-01-26T00:39:09Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:\n> > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n> > > +test_expect_success 'rebase when inside worktree subdirectory' '\n> > > +       git init main-wt &&\n> > > +       (\n> > > +               cd main-wt &&\n> > > +               git commit --allow-empty -m \"initial\" &&\n> > > +               # create commit with foo/bar/baz\n> > > +               mkdir -p foo/bar &&\n> > > +               touch foo/bar/baz &&\n> > > +               git add foo/bar/baz &&\n> > > +               git commit -m \"add foo/bar/baz\" &&\n> > > +               # create commit with a/b/c\n> > > +               mkdir -p a/b &&\n> > > +               touch a/b/c &&\n> > > +               git add a/b/c &&\n> > > +               git commit -m \"add a/b/c\" &&\n>\n> This is entirely minor, but all the inner subshells in this test are\n> superfluous. [...]\n\nOne other minor comment: If the file's timestamp has no significance\nto the test, then our style is to create the file with `>` rather than\n`touch`, so:\n\n    ... &&\n    >foo/bar/baz &&\n    ...\n    >a/b/c &&\n    ...\n"},{"id":"446907","messageId":"CABPp-BG2__OLFkOcJnH-V8SC3hrRRrB1zv+KEOiNfoQYB9Ehpw@mail.gmail.com","threadId":"56952","inReplyTo":"CAPig+cRCzUinKr0iQkDMK0Af4=47xKZJKphcuTRxt4BaxrYwfA@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-26T00:51:24Z","receivedAt":"2022-01-26T00:51:39Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>\n> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:\n> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n> > > > +test_expect_success 'rebase when inside worktree subdirectory' '\n> > > > +       git init main-wt &&\n> > > > +       (\n> > > > +               cd main-wt &&\n> > > > +               git commit --allow-empty -m \"initial\" &&\n> > > > +               # create commit with foo/bar/baz\n> > > > +               mkdir -p foo/bar &&\n> > > > +               touch foo/bar/baz &&\n> > > > +               git add foo/bar/baz &&\n> > > > +               git commit -m \"add foo/bar/baz\" &&\n> > > > +               # create commit with a/b/c\n> > > > +               mkdir -p a/b &&\n> > > > +               touch a/b/c &&\n> > > > +               git add a/b/c &&\n> > > > +               git commit -m \"add a/b/c\" &&\n> >\n> > This is entirely minor, but all the inner subshells in this test are\n> > superfluous. [...]\n>\n> One other minor comment: If the file's timestamp has no significance\n> to the test, then our style is to create the file with `>` rather than\n> `touch`, so:\n>\n>     ... &&\n>     >foo/bar/baz &&\n>     ...\n>     >a/b/c &&\n>     ...\n\nAh, good point.  And while at it, we can replace the touch/add/commit\nsequence with a simple test_commit call.\n"},{"id":"446908","messageId":"kl6l7dan1enb.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"56952","inReplyTo":"CABPp-BG2__OLFkOcJnH-V8SC3hrRRrB1zv+KEOiNfoQYB9Ehpw@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-01-26T01:15:04Z","receivedAt":"2022-01-26T01:15:08Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>>\n>> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:\n>> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n>> > > > +test_expect_success 'rebase when inside worktree subdirectory' '\n>> > > > +       git init main-wt &&\n>> > > > +       (\n>> > > > +               cd main-wt &&\n>> > > > +               git commit --allow-empty -m \"initial\" &&\n>> > > > +               # create commit with foo/bar/baz\n>> > > > +               mkdir -p foo/bar &&\n>> > > > +               touch foo/bar/baz &&\n>> > > > +               git add foo/bar/baz &&\n>> > > > +               git commit -m \"add foo/bar/baz\" &&\n>> > > > +               # create commit with a/b/c\n>> > > > +               mkdir -p a/b &&\n>> > > > +               touch a/b/c &&\n>> > > > +               git add a/b/c &&\n>> > > > +               git commit -m \"add a/b/c\" &&\n>> >\n>> > This is entirely minor, but all the inner subshells in this test are\n>> > superfluous. [...]\n>>\n>> One other minor comment: If the file's timestamp has no significance\n>> to the test, then our style is to create the file with `>` rather than\n>> `touch`, so:\n>>\n>>     ... &&\n>>     >foo/bar/baz &&\n>>     ...\n>>     >a/b/c &&\n>>     ...\n>\n> Ah, good point.  And while at it, we can replace the touch/add/commit\n> sequence with a simple test_commit call.\n\nYes, thanks for pointing that out - I had copied my reproduction script\ninto the test case without paying enough attention.\n"},{"id":"446909","messageId":"CABPp-BHwRLER2yWAwU88Fu6sw7ObMOr-HRN-jngo881+47WzYg@mail.gmail.com","threadId":"56952","inReplyTo":"kl6l7dan1enb.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-26T01:38:38Z","receivedAt":"2022-01-26T01:38:52Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jan 25, 2022 at 5:15 PM Glen Choo <chooglen@google.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > On Tue, Jan 25, 2022 at 4:39 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> >>\n> >> On Tue, Jan 25, 2022 at 7:32 PM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> >> > On Tue, Jan 25, 2022 at 6:59 PM Elijah Newren <newren@gmail.com> wrote:\n> >> > > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n> >> > > > +test_expect_success 'rebase when inside worktree subdirectory' '\n> >> > > > +       git init main-wt &&\n> >> > > > +       (\n> >> > > > +               cd main-wt &&\n> >> > > > +               git commit --allow-empty -m \"initial\" &&\n> >> > > > +               # create commit with foo/bar/baz\n> >> > > > +               mkdir -p foo/bar &&\n> >> > > > +               touch foo/bar/baz &&\n> >> > > > +               git add foo/bar/baz &&\n> >> > > > +               git commit -m \"add foo/bar/baz\" &&\n> >> > > > +               # create commit with a/b/c\n> >> > > > +               mkdir -p a/b &&\n> >> > > > +               touch a/b/c &&\n> >> > > > +               git add a/b/c &&\n> >> > > > +               git commit -m \"add a/b/c\" &&\n> >> >\n> >> > This is entirely minor, but all the inner subshells in this test are\n> >> > superfluous. [...]\n> >>\n> >> One other minor comment: If the file's timestamp has no significance\n> >> to the test, then our style is to create the file with `>` rather than\n> >> `touch`, so:\n> >>\n> >>     ... &&\n> >>     >foo/bar/baz &&\n> >>     ...\n> >>     >a/b/c &&\n> >>     ...\n> >\n> > Ah, good point.  And while at it, we can replace the touch/add/commit\n> > sequence with a simple test_commit call.\n>\n> Yes, thanks for pointing that out - I had copied my reproduction script\n> into the test case without paying enough attention.\n\nTo be honest, it's the kind of thing others have reminded me of dozens\nof times.  In fact, although I read your testcase, it didn't even\noccur to me until Eric brought up \">\" instead of \"touch\" (which was\nsomething else I had been reminded of dozens of times before\nlearning).\n\nTo me, it's all small potatoes compared to coming up with a good\nreproducible testcase; that was the hard work, and you did that.  You\neven found a fix and called out the right things in the review for\nothers to look at.  So, it's all good.\n"},{"id":"446932","messageId":"4cdb1e15-31d8-7fa4-b1f3-b43498685e13@gmail.com","threadId":"56952","inReplyTo":"CABPp-BFdD=f82QvQfokD346YT6aCQ=WwZ09S-a=BPXXj5_LZkg@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2022-01-26T11:00:09Z","receivedAt":"2022-01-26T11:00:13Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 25/01/2022 23:59, Elijah Newren wrote:\n> On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n>>\n>> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>\n>>> From: Elijah Newren <newren@gmail.com>\n>>>\n>>> Since rebase spawns a `checkout` subprocess, make sure we run that from\n>>> the startup_info->original_cwd directory, so that the checkout process\n>>> knows to protect that directory.\n>>>\n>>> Signed-off-by: Elijah Newren <newren@gmail.com>\n>>> ---\n>>>   sequencer.c          | 2 ++\n>>>   t/t2501-cwd-empty.sh | 4 ++--\n>>>   2 files changed, 4 insertions(+), 2 deletions(-)\n>>>\n>>> diff --git a/sequencer.c b/sequencer.c\n>>> index ea96837cde3..83f257e7fa4 100644\n>>> --- a/sequencer.c\n>>> +++ b/sequencer.c\n>>> @@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n>>>\n>>>        cmd.git_cmd = 1;\n>>>\n>>> +     if (startup_info->original_cwd)\n>>> +             cmd.dir = startup_info->original_cwd;\n>>>        strvec_push(&cmd.args, \"checkout\");\n>>>        strvec_push(&cmd.args, commit);\n>>>        strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\n>>> diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n>>> index b1182390ba3..52335a8afe9 100755\n>>> --- a/t/t2501-cwd-empty.sh\n>>> +++ b/t/t2501-cwd-empty.sh\n>>> @@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n>>>   '\n>>>\n>>>   test_expect_success 'rebase does not clean cwd incidentally' '\n>>> -     test_incidental_dir_removal failure git rebase reverted\n>>> +     test_incidental_dir_removal success git rebase reverted\n>>>   '\n>>>\n>>>   test_expect_success 'rebase fails if cwd needs to be removed' '\n>>> -     test_required_dir_removal failure git rebase fd_conflict\n>>> +     test_required_dir_removal success git rebase fd_conflict\n>>>   '\n>>>\n>>>   test_expect_success 'revert does not clean cwd incidentally' '\n>>> --\n>>> gitgitgadget\n>>\n>> This commit (which is already in master) introduces a bug that breaks\n>> rebase when rebasing inside a subdirectory of a worktree. You can see\n>> that the below test fails with:\n>>\n>>    error: The following untracked working tree files would be overwritten by merge:\n>>            a/b/c\n>>    Please move or remove them before you merge.\n> \n> Thanks for the detailed report -- with a full testcase!\n> \n>> This only affects subdirectories in worktrees, i.e. rebasing anywhere in\n>> the `main-wt` directory is fine, and rebasing from the top of `other-wt`\n>> is fine, but `other-wt/any/other/dir` fails.\n>>\n>> I haven't tracked down the root cause yet, but judging from the commit,\n>> I would suppose that the checkout is being spawned in the wrong\n>> directory, causing the files to not be cleaned up.\n> \n> There's nothing wrong with running checkout from a subdirectory.  It\n> is unfortunate that setup.c auto-discovers both the git directory and\n> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> the case of a non-main worktree; it's not particularly friendly for\n> subcommands.  Of course, it's also unfortunate that sequencer still\n> forks subprocesses other than those requested by a user with e.g.\n> --exec.\n> \n> But, anyway, I've got a patch that I'll send as soon as it passes CI\n> (https://github.com/git/git/pull/1205).\n\nThe patch hasn't come through to me on the mailing list yet, but it \nlooks good. I thought we set both GIT_DIR and GIT_WORK_TREE when we were \nin a non-main worktree but obviously we don't. Eric do you happen to \nknow if that is intentional?\n\nAs an aside I'm going to post an updated version of my series removing \nthe call to 'git checkout' from sequencer.c later today but we \ndefinitely want to have this fix before that series gets merged.\n\nBest Wishes\n\nPhillip\n\n>> ---\n>>   t/t3400-rebase.sh | 29 +++++++++++++++++++++++++++++\n>>   1 file changed, 29 insertions(+)\n>>\n>> diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh\n>> index 23dbd3c82e..8b8b66538b 100755\n>> --- a/t/t3400-rebase.sh\n>> +++ b/t/t3400-rebase.sh\n>> @@ -416,4 +416,33 @@ test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink'\n>>          mv actual_logs .git/logs\n>>   '\n>>\n>> +test_expect_success 'rebase when inside worktree subdirectory' '\n>> +       git init main-wt &&\n>> +       (\n>> +               cd main-wt &&\n>> +               git commit --allow-empty -m \"initial\" &&\n>> +               # create commit with foo/bar/baz\n>> +               mkdir -p foo/bar &&\n>> +               touch foo/bar/baz &&\n>> +               git add foo/bar/baz &&\n>> +               git commit -m \"add foo/bar/baz\" &&\n>> +               # create commit with a/b/c\n>> +               mkdir -p a/b &&\n>> +               touch a/b/c &&\n>> +               git add a/b/c &&\n>> +               git commit -m \"add a/b/c\" &&\n>> +               # create another branch for our other worktree\n>> +               git branch other &&\n>> +               git worktree add ../other-wt other &&\n>> +               (\n>> +                       cd ../other-wt &&\n>> +                       mkdir -p random/dir &&\n>> +                       (\n>> +                               cd random/dir &&\n>> +                               git rebase --onto HEAD^^ HEAD^  # drops the HEAD^ commit\n>> +                       )\n>> +               )\n>> +       )\n>> +'\n>> +\n>>   test_done\n>> --\n>> 2.35.0.rc0.227.g00780c9af4-goog\n\n"},{"id":"446981","messageId":"CAPig+cQVNdmHQnhORqh2XtJSMhcOymR99pmKTWOAyhoQ10khSw@mail.gmail.com","threadId":"56952","inReplyTo":"4cdb1e15-31d8-7fa4-b1f3-b43498685e13@gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-01-26T17:53:19Z","receivedAt":"2022-01-26T17:53:33Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> On 25/01/2022 23:59, Elijah Newren wrote:\n> > There's nothing wrong with running checkout from a subdirectory.  It\n> > is unfortunate that setup.c auto-discovers both the git directory and\n> > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> > the case of a non-main worktree; it's not particularly friendly for\n> > subcommands.  Of course, it's also unfortunate that sequencer still\n> > forks subprocesses other than those requested by a user with e.g.\n> > --exec.\n> >\n> > But, anyway, I've got a patch that I'll send as soon as it passes CI\n> > (https://github.com/git/git/pull/1205).\n>\n> The patch hasn't come through to me on the mailing list yet, but it\n> looks good. I thought we set both GIT_DIR and GIT_WORK_TREE when we were\n> in a non-main worktree but obviously we don't. Eric do you happen to\n> know if that is intentional?\n\nAs far as I know, there is no reason to set GIT_DIR and GIT_WORK_TREE,\nin general, when in a linked worktree since each worktree has its own\n.git file (\"gitfile\") which tells Git commands where the repository is\nand signals that that directory itself (which contains the .git file)\nis indeed a Git worktree.\n"},{"id":"446985","messageId":"CABPp-BHH0d9evJ5n-njKkPPsTQrY9E4Sh4Yb+Nq_JiYffuHUBQ@mail.gmail.com","threadId":"56952","inReplyTo":"kl6la6fj1gpb.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-26T19:04:57Z","receivedAt":"2022-01-26T19:05:12Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jan 25, 2022 at 4:30 PM Glen Choo <chooglen@google.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > On Tue, Jan 25, 2022 at 12:27 PM Glen Choo <chooglen@google.com> wrote:\n> >>\n> >> \"Elijah Newren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> >>\n> >> > From: Elijah Newren <newren@gmail.com>\n> >> >\n> >> > Since rebase spawns a `checkout` subprocess, make sure we run that from\n> >> > the startup_info->original_cwd directory, so that the checkout process\n> >> > knows to protect that directory.\n> >> >\n> >> > Signed-off-by: Elijah Newren <newren@gmail.com>\n> >> > ---\n> >> >  sequencer.c          | 2 ++\n> >> >  t/t2501-cwd-empty.sh | 4 ++--\n> >> >  2 files changed, 4 insertions(+), 2 deletions(-)\n> >> >\n> >> > diff --git a/sequencer.c b/sequencer.c\n> >> > index ea96837cde3..83f257e7fa4 100644\n> >> > --- a/sequencer.c\n> >> > +++ b/sequencer.c\n> >> > @@ -4228,6 +4228,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts,\n> >> >\n> >> >       cmd.git_cmd = 1;\n> >> >\n> >> > +     if (startup_info->original_cwd)\n> >> > +             cmd.dir = startup_info->original_cwd;\n> >> >       strvec_push(&cmd.args, \"checkout\");\n> >> >       strvec_push(&cmd.args, commit);\n> >> >       strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION \"=%s\", action);\n> >> > diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n> >> > index b1182390ba3..52335a8afe9 100755\n> >> > --- a/t/t2501-cwd-empty.sh\n> >> > +++ b/t/t2501-cwd-empty.sh\n> >> > @@ -166,11 +166,11 @@ test_expect_success 'cherry-pick fails if cwd needs to be removed' '\n> >> >  '\n> >> >\n> >> >  test_expect_success 'rebase does not clean cwd incidentally' '\n> >> > -     test_incidental_dir_removal failure git rebase reverted\n> >> > +     test_incidental_dir_removal success git rebase reverted\n> >> >  '\n> >> >\n> >> >  test_expect_success 'rebase fails if cwd needs to be removed' '\n> >> > -     test_required_dir_removal failure git rebase fd_conflict\n> >> > +     test_required_dir_removal success git rebase fd_conflict\n> >> >  '\n> >> >\n> >> >  test_expect_success 'revert does not clean cwd incidentally' '\n> >> > --\n> >> > gitgitgadget\n> >>\n> >> This commit (which is already in master) introduces a bug that breaks\n> >> rebase when rebasing inside a subdirectory of a worktree. You can see\n> >> that the below test fails with:\n> >>\n> >>   error: The following untracked working tree files would be overwritten by merge:\n> >>           a/b/c\n> >>   Please move or remove them before you merge.\n> >\n> > Thanks for the detailed report -- with a full testcase!\n>\n> Glad to be of help :)\n>\n> >> This only affects subdirectories in worktrees, i.e. rebasing anywhere in\n> >> the `main-wt` directory is fine, and rebasing from the top of `other-wt`\n> >> is fine, but `other-wt/any/other/dir` fails.\n> >>\n> >> I haven't tracked down the root cause yet, but judging from the commit,\n> >> I would suppose that the checkout is being spawned in the wrong\n> >> directory, causing the files to not be cleaned up.\n> >\n> > There's nothing wrong with running checkout from a subdirectory.  It\n> > is unfortunate that setup.c auto-discovers both the git directory and\n> > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> > the case of a non-main worktree; it's not particularly friendly for\n> > subcommands.  Of course, it's also unfortunate that sequencer still\n> > forks subprocesses other than those requested by a user with e.g.\n> > --exec.\n> >\n> > But, anyway, I've got a patch that I'll send as soon as it passes CI\n> > (https://github.com/git/git/pull/1205).\n>\n> I wish I had seen this email before diving into debugging it myself\n> since your fix is more comprehensive, but it was a good learning\n> experience nonetheless.\n\nYeah, sorry, it took me quite a bit of debugging as well before\nfiguring all this stuff out, so I don't think I sent the email in time\nto save you the work.\n"},{"id":"447062","messageId":"b56c8855-df66-4e6d-6bc8-01cd06dd0feb@gmail.com","threadId":"56952","inReplyTo":"CAPig+cQVNdmHQnhORqh2XtJSMhcOymR99pmKTWOAyhoQ10khSw@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2022-01-27T11:01:37Z","receivedAt":"2022-01-27T11:01:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 26/01/2022 17:53, Eric Sunshine wrote:\n> On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>> On 25/01/2022 23:59, Elijah Newren wrote:\n>>> There's nothing wrong with running checkout from a subdirectory.  It\n>>> is unfortunate that setup.c auto-discovers both the git directory and\n>>> the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n>>> the case of a non-main worktree; it's not particularly friendly for\n>>> subcommands.  Of course, it's also unfortunate that sequencer still\n>>> forks subprocesses other than those requested by a user with e.g.\n>>> --exec.\n>>>\n>>> But, anyway, I've got a patch that I'll send as soon as it passes CI\n>>> (https://github.com/git/git/pull/1205).\n>>\n>> The patch hasn't come through to me on the mailing list yet, but it\n>> looks good. I thought we set both GIT_DIR and GIT_WORK_TREE when we were\n>> in a non-main worktree but obviously we don't. Eric do you happen to\n>> know if that is intentional?\n> \n> As far as I know, there is no reason to set GIT_DIR and GIT_WORK_TREE,\n> in general, when in a linked worktree since each worktree has its own\n> .git file (\"gitfile\") which tells Git commands where the repository is\n> and signals that that directory itself (which contains the .git file)\n> is indeed a Git worktree.\n\nOh of course I should have thought of that. Now I'm wondering why we're \nexporting GIT_DIR - I should go and read the code for myself.\n\nThanks\n\nPhillip\n"},{"id":"447123","messageId":"20220127200341.333996-1-newren@gmail.com","threadId":"56952","inReplyTo":"CAPig+cQVNdmHQnhORqh2XtJSMhcOymR99pmKTWOAyhoQ10khSw@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-01-27T20:03:41Z","receivedAt":"2022-01-27T20:04:09Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 26, 2022 at 9:53 AM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>\n> On Wed, Jan 26, 2022 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> > On 25/01/2022 23:59, Elijah Newren wrote:\n> > > There's nothing wrong with running checkout from a subdirectory.  It\n> > > is unfortunate that setup.c auto-discovers both the git directory and\n> > > the working tree, but sets GIT_DIR without setting GIT_WORK_TREE in\n> > > the case of a non-main worktree; it's not particularly friendly for\n> > > subcommands.  Of course, it's also unfortunate that sequencer still\n> > > forks subprocesses other than those requested by a user with e.g.\n> > > --exec.\n> > >\n> > > But, anyway, I've got a patch that I'll send as soon as it passes CI\n> > > (https://github.com/git/git/pull/1205).\n> >\n> > The patch hasn't come through to me on the mailing list yet, but it\n> > looks good. I thought we set both GIT_DIR and GIT_WORK_TREE when we were\n> > in a non-main worktree but obviously we don't. Eric do you happen to\n> > know if that is intentional?\n>\n> As far as I know, there is no reason to set GIT_DIR and GIT_WORK_TREE,\n> in general, when in a linked worktree since each worktree has its own\n> .git file (\"gitfile\") which tells Git commands where the repository is\n> and signals that that directory itself (which contains the .git file)\n> is indeed a Git worktree.\n\nOh, interesting.  Not setting GIT_DIR either does sound a bit better.\n\n...though after digging for a while, it turns out to be a bit more\ninvolved than I thought.  Although the below patch passes our current\ntestsuite and fixes the reported bug, I'm worried I've missed some cases\nnot tested by the testsuite.\n\nNot sure if we want to pursue this, drop it, or something else.  Thoughts?\n\n-- >8 --\nSubject: [RFC/POC PATCH] setup: do not pre-emptively set GIT_DIR based on discovery\n\nsetup_git_directory_gently() handles a few cases, but when it discovers\nthe git directory and finds it is not \".git\", it sets and exports the\nGIT_DIR environment variable to the discovered value.  It does not set\nGIT_WORK_TREE, though, even when it also discovers it.  This has two\ndrawbacks:\n\n  * Since Git assumes the working tree is '.' when GIT_DIR is set and\n    GIT_WORK_TREE isn't, any subprocesses called by Git which need to\n    operate on the working tree either need to be careful to always\n    invoke with their cwd being the toplevel of the actual working tree\n    (and potentially losing information about 'prefix'), or take care to\n    also specify the GIT_WORK_TREE environment value when forking.\n\n  * The GIT_DIR environment variable behaves somewhat like an implicit\n    'the_repository'; it's another bit of global state that is\n    potentially problematic for libifying efforts of libgit.\n\nAvoid these issues by no longer exporting GIT_DIR from the setup code\n(other locations like the parser handling the --git-dir flag will still\nset it, though).\n\nSome comments on the various code changes:\n   * clone/push/fetch related:\n     * there are *many* subprocesses involved in fetch/push and friends,\n       and they typically need to know the GIT_DIR they are operating on\n     * this involves: fetch-patch.c, connected.c, bundle.c, clone.c,\n       transport-helper.c, receive-pack.c, upload-pack.c\n     * this accounts for the majority of this patch\n     * much of this work could be avoided by having enter_repo() call\n       xsetenv(GIT_DIR_ENVIRONMENT, \".\", 1) just after its set_git_dir()\n       call, but I don't know if that'd be considered a half measure\n   * rev-parse.c: substitute usage of the GIT_DIR environment variable\n     with the_repository->gitdir that should have the same value\n     (exept when the latter is \".git\" the former will be NULL, so there\n     are some small logic tweaks).\n   * run-command: hooks have been operating under the assumption that\n     GIT_DIR is set for a long time.  This is possibly\n     counter-productive for the same reasons listed above, and it has\n     certainly been brought up before on the list, but backward\n     compatibility concerns led me to just propagate it to avoid\n     breaking anything.\n   * test-subprocess: not sure if we should automatically export\n     GIT_DIR, but the existing code and one test in the repo relied on\n     it, so I just included it.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/clone.c            |  7 ++++++-\n builtin/receive-pack.c     | 12 ++++++++++++\n builtin/rev-parse.c        |  8 +++-----\n bundle.c                   |  2 ++\n connected.c                |  2 ++\n environment.c              |  1 -\n fetch-pack.c               |  6 ++++++\n run-command.c              |  2 ++\n t/helper/test-subprocess.c |  2 ++\n transport-helper.c         |  3 +++\n upload-pack.c              |  2 ++\n 11 files changed, 40 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 727e16e0ae..5304939cec 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -836,13 +836,18 @@ static void dissociate_from_references(void)\n {\n \tstatic const char* argv[] = { \"repack\", \"-a\", \"-d\", NULL };\n \tchar *alternates = git_pathdup(\"objects/info/alternates\");\n+\tstruct strvec env = STRVEC_INIT;\n+\tstrvec_pushf(&env, GIT_DIR_ENVIRONMENT \"=%s\", the_repository->gitdir);\n \n \tif (!access(alternates, F_OK)) {\n-\t\tif (run_command_v_opt(argv, RUN_GIT_CMD|RUN_COMMAND_NO_STDIN))\n+\t\tif (run_command_v_opt_cd_env(argv,\n+\t\t\t\t\t     RUN_GIT_CMD|RUN_COMMAND_NO_STDIN,\n+\t\t\t\t\t     NULL, env.v))\n \t\t\tdie(_(\"cannot repack to clean up\"));\n \t\tif (unlink(alternates) && errno != ENOENT)\n \t\t\tdie_errno(_(\"cannot unlink temporary alternates file\"));\n \t}\n+\tstrvec_clear(&env);\n \tfree(alternates);\n }\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 9f4a0b816c..b5d2633b78 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -825,6 +825,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n \tproc.stdout_to_stderr = 1;\n \tproc.trace2_hook_name = hook_name;\n \n+\tstrvec_pushf(&proc.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \tif (feed_state->push_options) {\n \t\tint i;\n \t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n@@ -953,6 +955,8 @@ static int run_update_hook(struct command *cmd)\n \tstrvec_push(&proc.args, cmd->ref_name);\n \tstrvec_push(&proc.args, oid_to_hex(&cmd->old_oid));\n \tstrvec_push(&proc.args, oid_to_hex(&cmd->new_oid));\n+\tstrvec_pushf(&proc.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \n \tproc.no_stdin = 1;\n \tproc.stdout_to_stderr = 1;\n@@ -1128,6 +1132,8 @@ static int run_proc_receive_hook(struct command *commands,\n \t}\n \n \tstrvec_push(&proc.args, hook_path);\n+\tstrvec_pushf(&proc.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \tproc.in = -1;\n \tproc.out = -1;\n \tproc.trace2_hook_name = \"proc-receive\";\n@@ -1645,6 +1651,8 @@ static void run_update_post_hook(struct command *commands)\n \tproc.stdout_to_stderr = 1;\n \tproc.err = use_sideband ? -1 : 0;\n \tproc.trace2_hook_name = \"post-update\";\n+\tstrvec_pushf(&proc.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \n \tif (!start_command(&proc)) {\n \t\tif (use_sideband)\n@@ -2236,6 +2244,8 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n+\t\tstrvec_pushf(&child.env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \t\tstatus = run_command(&child);\n \t\tif (status)\n \t\t\treturn \"unpack-objects abnormal exit\";\n@@ -2267,6 +2277,8 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n+\t\tstrvec_pushf(&child.env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \t\tstatus = start_command(&child);\n \t\tif (status)\n \t\t\treturn \"index-pack fork failed\";\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 8480a59f57..1d578d48a1 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -930,12 +930,12 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t}\n \t\t\tif (!strcmp(arg, \"--git-dir\") ||\n \t\t\t    !strcmp(arg, \"--absolute-git-dir\")) {\n-\t\t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n+\t\t\t\tconst char *gitdir = the_repository->gitdir;\n \t\t\t\tchar *cwd;\n \t\t\t\tint len;\n \t\t\t\tenum format_type wanted = format;\n \t\t\t\tif (arg[2] == 'g') {\t/* --git-dir */\n-\t\t\t\t\tif (gitdir) {\n+\t\t\t\t\tif (strcmp(gitdir, \".git\")) {\n \t\t\t\t\t\tprint_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);\n \t\t\t\t\t\tcontinue;\n \t\t\t\t\t}\n@@ -945,9 +945,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t}\n \t\t\t\t} else {\t\t/* --absolute-git-dir */\n \t\t\t\t\twanted = FORMAT_CANONICAL;\n-\t\t\t\t\tif (!gitdir && !prefix)\n-\t\t\t\t\t\tgitdir = \".git\";\n-\t\t\t\t\tif (gitdir) {\n+\t\t\t\t\tif (strcmp(gitdir, \".git\") || !prefix) {\n \t\t\t\t\t\tstruct strbuf realpath = STRBUF_INIT;\n \t\t\t\t\t\tstrbuf_realpath(&realpath, gitdir, 1);\n \t\t\t\t\t\tputs(realpath.buf);\ndiff --git a/bundle.c b/bundle.c\nindex a0bb687b0f..897d36c83a 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -573,6 +573,8 @@ int unbundle(struct repository *r, struct bundle_header *header,\n {\n \tstruct child_process ip = CHILD_PROCESS_INIT;\n \tstrvec_pushl(&ip.args, \"index-pack\", \"--fix-thin\", \"--stdin\", NULL);\n+\tstrvec_pushf(&ip.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \n \tif (extra_index_pack_args) {\n \t\tstrvec_pushv(&ip.args, extra_index_pack_args->v);\ndiff --git a/connected.c b/connected.c\nindex ed3025e7a2..3dfc6278e8 100644\n--- a/connected.c\n+++ b/connected.c\n@@ -109,6 +109,8 @@ int check_connected(oid_iterate_fn fn, void *cb_data,\n \t\t\t     _(\"Checking connectivity\"));\n \n \trev_list.git_cmd = 1;\n+\tstrvec_pushf(&rev_list.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \tif (opt->env)\n \t\tstrvec_pushv(&rev_list.env_array, opt->env);\n \trev_list.in = -1;\ndiff --git a/environment.c b/environment.c\nindex fd0501e77a..db31c37a62 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -327,7 +327,6 @@ char *get_graft_file(struct repository *r)\n \n static void set_git_dir_1(const char *path)\n {\n-\txsetenv(GIT_DIR_ENVIRONMENT, path, 1);\n \tsetup_git_env(path);\n }\n \ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex dd6ec449f2..8b59014c3a 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -892,6 +892,8 @@ static int get_pack(struct fetch_pack_args *args,\n \t\tcmd_name = \"index-pack\";\n \t\tstrvec_push(&cmd.args, cmd_name);\n \t\tstrvec_push(&cmd.args, \"--stdin\");\n+\t\tstrvec_pushf(&cmd.env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \t\tif (!args->quiet && !args->no_progress)\n \t\t\tstrvec_push(&cmd.args, \"-v\");\n \t\tif (args->use_thin_pack)\n@@ -927,6 +929,8 @@ static int get_pack(struct fetch_pack_args *args,\n \t}\n \telse {\n \t\tcmd_name = \"unpack-objects\";\n+\t\tstrvec_pushf(&cmd.env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \t\tstrvec_push(&cmd.args, cmd_name);\n \t\tif (args->quiet || args->no_progress)\n \t\t\tstrvec_push(&cmd.args, \"-q\");\n@@ -1703,6 +1707,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \t\t\tstrvec_pushf(&cmd.args, \"--index-pack-arg=%s\",\n \t\t\t\t     index_pack_args.v[j]);\n \t\tstrvec_push(&cmd.args, uri);\n+\t\tstrvec_pushf(&cmd.env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \t\tcmd.git_cmd = 1;\n \t\tcmd.no_stdin = 1;\n \t\tcmd.out = -1;\ndiff --git a/run-command.c b/run-command.c\nindex 69dde42f1e..0fb21f92bf 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1319,6 +1319,8 @@ int run_hook_ve(const char *const *env, const char *name, va_list args)\n \tstrvec_push(&hook.args, p);\n \twhile ((p = va_arg(args, const char *)))\n \t\tstrvec_push(&hook.args, p);\n+\tstrvec_pushf(&hook.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \tif (env)\n \t\tstrvec_pushv(&hook.env_array, (const char **)env);\n \thook.no_stdin = 1;\ndiff --git a/t/helper/test-subprocess.c b/t/helper/test-subprocess.c\nindex ff22f2fa2c..d58bf7e18d 100644\n--- a/t/helper/test-subprocess.c\n+++ b/t/helper/test-subprocess.c\n@@ -16,5 +16,7 @@ int cmd__subprocess(int argc, const char **argv)\n \t}\n \tcp.git_cmd = 1;\n \tstrvec_pushv(&cp.args, (const char **)argv + 1);\n+\tstrvec_pushf(&cp.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \treturn run_command(&cp);\n }\ndiff --git a/transport-helper.c b/transport-helper.c\nindex a0297b0986..d26b5ecd10 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -448,6 +448,9 @@ static int get_importer(struct transport *transport, struct child_process *fasti\n \t\tstrvec_pushf(&fastimport->args, \"--cat-blob-fd=%d\", cat_blob_fd);\n \t}\n \tfastimport->git_cmd = 1;\n+\tif (have_git_dir())\n+\t\tstrvec_pushf(&fastimport->env_array, \"%s=%s\",\n+\t\t\t     GIT_DIR_ENVIRONMENT, get_git_dir());\n \n \tcode = start_command(fastimport);\n \treturn code;\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 8acc98741b..7bf36d6d3a 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -295,6 +295,8 @@ static void create_pack_file(struct upload_pack_data *pack_data,\n \t\tstrvec_push(&pack_objects.args, \"--shallow-file\");\n \t\tstrvec_push(&pack_objects.args, \"\");\n \t}\n+\tstrvec_pushf(&pack_objects.env_array, \"%s=%s\",\n+\t\t     GIT_DIR_ENVIRONMENT, the_repository->gitdir);\n \tstrvec_push(&pack_objects.args, \"pack-objects\");\n \tstrvec_push(&pack_objects.args, \"--revs\");\n \tif (pack_data->use_thin_pack)\n-- \n2.35.0.2.g9420af4777.dirty\n\n"},{"id":"447820","messageId":"CAPig+cSi8_90=-Fvt_fq=VtOW_HzifNhrk1gaa6F1GrEonng+Q@mail.gmail.com","threadId":"56952","inReplyTo":"20220127200341.333996-1-newren@gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-02-05T11:23:57Z","receivedAt":"2022-02-05T11:24:10Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:\n> On Wed, Jan 26, 2022 at 9:53 AM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> > As far as I know, there is no reason to set GIT_DIR and GIT_WORK_TREE,\n> > in general, when in a linked worktree since each worktree has its own\n> > .git file (\"gitfile\") which tells Git commands where the repository is\n> > and signals that that directory itself (which contains the .git file)\n> > is indeed a Git worktree.\n>\n> Oh, interesting.  Not setting GIT_DIR either does sound a bit better.\n>\n> ...though after digging for a while, it turns out to be a bit more\n> involved than I thought.  Although the below patch passes our current\n> testsuite and fixes the reported bug, I'm worried I've missed some cases\n> not tested by the testsuite.\n>\n> Not sure if we want to pursue this, drop it, or something else.  Thoughts?\n\nIt's an enticing idea, though I have no deep knowledge about all the\npossible interactions which may be impacted by such a change. Duy had\na deep understanding of how all this worked, and probably Peff, as\nwell, but they aren't around to offer opinions.\n\nset_git_dir() has been setting the GIT_DIR environment variable ever\nsince it (the function) was introduced by d7ac12b25d (Add\nset_git_dir() function, 2007-08-01). Unfortunately, the commit message\ndoesn't explain why it does so.\n\nMore below...\n\n> -- >8 --\n> Subject: [RFC/POC PATCH] setup: do not pre-emptively set GIT_DIR based on discovery\n>\n> Some comments on the various code changes:\n>    * clone/push/fetch related:\n>      * there are *many* subprocesses involved in fetch/push and friends,\n>        and they typically need to know the GIT_DIR they are operating on\n>      * this involves: fetch-patch.c, connected.c, bundle.c, clone.c,\n>        transport-helper.c, receive-pack.c, upload-pack.c\n>      * this accounts for the majority of this patch\n>      * much of this work could be avoided by having enter_repo() call\n>        xsetenv(GIT_DIR_ENVIRONMENT, \".\", 1) just after its set_git_dir()\n>        call, but I don't know if that'd be considered a half measure\n\nIt does feel a bit like a bandaid to insert new code at these\nlocations to set GIT_DIR manually. It's not clear to readers why\nGIT_DIR is needed for these specific cases, nor what the impact is\nwhen it is not set. Thus, one wonders if such a blanket approach is\nindeed required or if a more narrow and directed fix can be applied,\nsuch as calling subprograms with an explicit --git-dir= rather than\nsetting GIT_DIR with its potentially more broad and\ndifficult-to-reason-about impact.\n\n> Signed-off-by: Elijah Newren <newren@gmail.com>\n> ---\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> @@ -836,13 +836,18 @@ static void dissociate_from_references(void)\n>  {\n> +       struct strvec env = STRVEC_INIT;\n> +       strvec_pushf(&env, GIT_DIR_ENVIRONMENT \"=%s\", the_repository->gitdir);\n\nMinor inconsistency: all the other similar changes in this patch use\n\"%s=%s\" and then pass in GIT_DIR_ENVIRONMENT to be interpolated by\n`%s`.\n\n> diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\n> @@ -930,12 +930,12 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n> -                               const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n> +                               const char *gitdir = the_repository->gitdir;\n>                                 if (arg[2] == 'g') {    /* --git-dir */\n> -                                       if (gitdir) {\n> +                                       if (strcmp(gitdir, \".git\")) {\n> @@ -945,9 +945,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n> -                                       if (!gitdir && !prefix)\n> -                                               gitdir = \".git\";\n> -                                       if (gitdir) {\n> +                                       if (strcmp(gitdir, \".git\") || !prefix) {\n\nThe meaning here becomes more obscure with this change applied. In the\noriginal code, it was obvious enough that non-NULL `gitdir` meant that\nthe GIT_DIR environment variable had a value, but\n`strcmp(gitdir,\".git\")` probably doesn't convey much to readers of\nthis code? Assigning the result of the strcmp() to a well-named\nvariable could go a long way toward making the meaning clearer. Or, an\nin-code comment might be warranted.\n\n> diff --git a/environment.c b/environment.c\n> @@ -327,7 +327,6 @@ char *get_graft_file(struct repository *r)\n>  static void set_git_dir_1(const char *path)\n>  {\n> -       xsetenv(GIT_DIR_ENVIRONMENT, path, 1);\n>         setup_git_env(path);\n>  }\n"},{"id":"447821","messageId":"CAPig+cRCN=QQHupg1eOx9YvsLhgn+dDN6eYvQL6JhzknCa_3=w@mail.gmail.com","threadId":"56952","inReplyTo":"CAPig+cSi8_90=-Fvt_fq=VtOW_HzifNhrk1gaa6F1GrEonng+Q@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2022-02-05T11:42:40Z","receivedAt":"2022-02-05T11:42:53Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Feb 5, 2022 at 6:23 AM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:\n> > Some comments on the various code changes:\n> >    * clone/push/fetch related:\n> >      * there are *many* subprocesses involved in fetch/push and friends,\n> >        and they typically need to know the GIT_DIR they are operating on\n> >      * this involves: fetch-patch.c, connected.c, bundle.c, clone.c,\n> >        transport-helper.c, receive-pack.c, upload-pack.c\n> >      * this accounts for the majority of this patch\n>\n> It does feel a bit like a bandaid to insert new code at these\n> locations to set GIT_DIR manually. It's not clear to readers why\n> GIT_DIR is needed for these specific cases, nor what the impact is\n> when it is not set. Thus, one wonders if such a blanket approach is\n> indeed required or if a more narrow and directed fix can be applied,\n> such as calling subprograms with an explicit --git-dir= rather than\n> setting GIT_DIR with its potentially more broad and\n> difficult-to-reason-about impact.\n\nI meant to ask here what was the nature of the various failures you\nwere seeing without GIT_DIR being set, and whether you had considered\ntackling those failures with --git-dir= instead of GIT_DIR. If so,\nwere the problems too difficult to overcome by --git-dir= alone?\nFleshing out the commit message with such information might be\nworthwhile.\n\nBy the way, I also didn't mean to imply that the\n\"difficult-to-reason-about GIT_DIR interaction in relation to\nsubprograms\" problem is new to this patch; it isn't. That problem has\nbeen around for ages (at least since 2007-08-01) but it wasn't as\nobvious since the setting of GIT_DIR was so far removed from other\ncode which runs subprograms, thus readers likely wouldn't be thinking\nabout GIT_DIR when reading the code which runs subprograms. This patch\nonly makes the problem more obvious since readers now see the\nsequence:\n\n    (1) set GIT_DIR\n    (2) launch subprogram\n\nSo, a reader is more likely to wonder why GIT_DIR is needed for the\nsubprogram and what possible wider side-effects it might have.\n"},{"id":"447827","messageId":"CABPp-BEd6ZoOSaA1agmaDX8q2Q7fAvoDfiPPTbKv6AatLvsHaw@mail.gmail.com","threadId":"56952","inReplyTo":"CAPig+cRCN=QQHupg1eOx9YvsLhgn+dDN6eYvQL6JhzknCa_3=w@mail.gmail.com","subject":"Re: [Bug] Rebase from worktree subdir is broken (was Re: [PATCH v5 07/11] rebase: do not attempt to remove startup_info->original_cwd)","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2022-02-05T22:35:58Z","receivedAt":"2022-02-05T22:36:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sat, Feb 5, 2022 at 3:42 AM Eric Sunshine <sunshine@sunshineco.com> wrote:\n>\n> On Sat, Feb 5, 2022 at 6:23 AM Eric Sunshine <sunshine@sunshineco.com> wrote:\n> > On Thu, Jan 27, 2022 at 3:03 PM Elijah Newren <newren@gmail.com> wrote:\n> > > Some comments on the various code changes:\n> > >    * clone/push/fetch related:\n> > >      * there are *many* subprocesses involved in fetch/push and friends,\n> > >        and they typically need to know the GIT_DIR they are operating on\n> > >      * this involves: fetch-patch.c, connected.c, bundle.c, clone.c,\n> > >        transport-helper.c, receive-pack.c, upload-pack.c\n> > >      * this accounts for the majority of this patch\n> >\n> > It does feel a bit like a bandaid to insert new code at these\n> > locations to set GIT_DIR manually. It's not clear to readers why\n> > GIT_DIR is needed for these specific cases, nor what the impact is\n> > when it is not set. Thus, one wonders if such a blanket approach is\n> > indeed required or if a more narrow and directed fix can be applied,\n> > such as calling subprograms with an explicit --git-dir= rather than\n> > setting GIT_DIR with its potentially more broad and\n> > difficult-to-reason-about impact.\n>\n> I meant to ask here what was the nature of the various failures you\n> were seeing without GIT_DIR being set, and whether you had considered\n> tackling those failures with --git-dir= instead of GIT_DIR. If so,\n> were the problems too difficult to overcome by --git-dir= alone?\n> Fleshing out the commit message with such information might be\n> worthwhile.\n\nclone, receive-pack, etc. will often spawn subprocesses such as\nrepack, or index-pack.  Those kinds of commands will not have been\ninvoked from within a particular git directory, so discovery in the\nsubprocess either doesn't find the git directory or finds the wrong\none (e.g. testcases where you're in a repo and do `git clone .\nmy-clone` would cause subprocesses to use discovery and find the outer\ngit repo rather than the 'my-clone' they are supposed to be working\non).\n\nI didn't look at just --git-dir=.  I probably could have, but since I\nwas just trying to get a feel for how big of a change it was, and\nthere's the possibility of nested subprocesses (clone forking some\nhttp-fetch thingy, which forks an index-pack subprocess, etc.), so I\njust did the environment variable to save the work from diving down\nthe hierarchy.\n\nI agree that if we want to make this kind of change, a better commit\nmessage would probably be in order, and perhaps trying out --git-dir=\ninstead of setting the environment variable.  However, after seeing\nthe patch myself and thinking about it for another week, this just\ndoesn't seem like a change that's worth it to me.  If someone else\nwants to pick up the patch and run with it, they should feel free, but\nI'm not motivated enough to do so.\n\n> By the way, I also didn't mean to imply that the\n> \"difficult-to-reason-about GIT_DIR interaction in relation to\n> subprograms\" problem is new to this patch; it isn't. That problem has\n> been around for ages (at least since 2007-08-01) but it wasn't as\n> obvious since the setting of GIT_DIR was so far removed from other\n> code which runs subprograms, thus readers likely wouldn't be thinking\n> about GIT_DIR when reading the code which runs subprograms. This patch\n> only makes the problem more obvious since readers now see the\n> sequence:\n>\n>     (1) set GIT_DIR\n>     (2) launch subprogram\n>\n> So, a reader is more likely to wonder why GIT_DIR is needed for the\n> subprogram and what possible wider side-effects it might have.\n\nYes, well put.\n"},{"id":"451124","messageId":"220311.86tuc4hf4p.gmgdl@evledraar.gmail.com","threadId":"56952","inReplyTo":"1fd76d4f8a8c98d3c78c94cc8f3d36c335bd3bfb.1639026515.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 01/11] t2501: add various tests for removing the current working directory","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T11:57:35Z","receivedAt":"2022-03-11T12:01:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 09 2021, Elijah Newren via GitGitGadget wrote:\n\n> From: Elijah Newren <newren@gmail.com>\n> [...]\n> +test_expect_success 'git version continues working from a deleted dir' '\n> +\tmkdir tmp &&\n> +\t(\n> +\t\tcd tmp &&\n> +\t\trm -rf ../tmp &&\n> +\t\tgit version\n> +\t)\n> +'\n\nThere's a (minor) regression here in v2.35.0 on AIX (as of 8a0d52dfd87\n(t2501: add various tests for removing the current working directory,\n2021-12-09)). The tooling on that OS will also refuse to remove the cwd:\n    \n    + mkdir tmp\n    + cd tmp\n    + rm -rf ../tmp\n    rm: Cannot remove the current directory ../tmp.\n    error: last command exited with $?=2\n    not ok 21 - git version continues working from a deleted dir\n    #\n\nJust splitting that into a prereq that checks if the first two lines of\nthe sub-shell works should be a solution in this case, but I only had\ntime for this quick bug report :)\n"}]}