Skip to content

gix clean with -r or -xd deletes the repo's own nested worktrees #1464

Closed
@EliahKagan

Description

@EliahKagan

Current behavior 😯

When a worktree has been created with git worktree add inside the repository, gix clean treats it the same as any untracked nested repository, even though it is a working tree of the current repository.

  • gix clean -re will delete it.
  • If * is listed in .gitignore, then gix clean -xde will also delete it, even without -r.

In both cases, this happens even if there is no intermediate ignored directory.

Significance

Users shouldn't usually create worktrees nested inside the main worktree (or inside each other). But it is supported to do so, and users may occasionally do so deliberately.

It is also easy to do by accident if one is not experienced with git worktree, since the one-argument form of git worktree add takes a path from which a branch is inferred, rather than taking a branch. git worktree mybranch creates a mybranch branch if it does not exist, and creates a worktree for it in the mybranch subdirectory of the current directory.

Since such a worktree works fine and does not usually interfere with the main worktree, even though the user may have meant to write git worktree ../mybranch, the user may proceed to use the nested worktree and to rely on data there not being lost due to cleaning in the main worktree.

In addition to being an area where I think the behavior is unexpected and carries some risk of inadvertent data loss, the question of how gix clean should treat nested worktrees is relevant to improving the help text for -r (especially if the recommendation I make below that they be given the same protection as submodule working trees is not followed).

Relationship to #1458

In all observations and manual testing described in this issue, I made sure to use a build containing the changes from #1462.

#1462 did fix #1458. The case in this issue is something I had not thought of at that time and did not include in #1458. This issue may be seen as a sequel to that one.

Expected behavior 🤔

It seems to me that:

  • Working trees managed by git worktree should be protected to the same degree as submodules' working trees.
  • No working tree of the repository gix clean is run in should ever be removed by any gix clean command, regardless of the options passed.

It seems to me that this can be avoided without much overhead even for worktrees that are nested inside ignored directories, because worktrees are discoverable by examining the repository.

(They have directories in .git/worktrees. This is of course with the usual variations when $GIT_DIR has a nonstandard value, or when .git is a regular file due to them being worktrees of a submodule.)

Edge cases

Strange edge cases are possible, such as a submodule with a git worktree managed worktree in a subdirectory of its superproject's working tree or of a sibling submodule's working tree, or a superproject with a git worktree managed worktree in a subdirectory of a submodule's working tree.

I am not sure what to expect, for what happens when gix clean is run the repository whose working tree contains the related repository's extra worktree. I lean slightly toward thinking it should be deleted in this situation, if a nested repository would otherwise be deleted. This seems like the simplest approach to implement.

I do not think these edge cases, even if the related repository's working tree is still to be deleted, challenge the expectations given above.

  • A repository has a close relationship to all its working trees, including git worktree managed working trees.
  • They are more closely related to each other, conceptually, than a repository is related to its own submodules--for example, a repository's local configuration is not used in its submodules.
  • The reason gix clean does not and should not remove submodules is that they are tracked. This is different from the reason gix clean should not remove its own worktrees, which is that they have the same status as the repository's own main working tree or .git directory.
  • By the point we get to worktrees belonging to submodules, superprojects, or sibling submodules, we are conceptually much further removed, such that the situation is closer to that of a separately cloned repository that would be eligible for removal with -r and in some cases without -r when also ignored.

When the current repository is a submodule, of course its own git worktree managed working trees should be protected from deletion when git clean commands are run within it. That's conceptually identical to a situation with git worktree managed worktrees where no submodules are involved in any way. The only difference is that to find out where to look to find out where the worktrees are, we have to follow the path in the .git file (or use a mechanism that already takes care of this).

Git behavior

git clean does not remove them with any combination of options.

As in #1458, this is area where git behavior is not decisive because git clean does not remove nested repositories, ignored or otherwise. However, also as in #1458, the git clean behavior is is relevant here because of the expectations it sets about what cleaning means.

Steps to reproduce 🕹

With -r and not -x or -d – instructions

Create a repository with at least one commit:

git init with-worktree
cd with-worktree
touch file
git add .
git commit -m 'Initial commit'

Add a worktree for a new feature branch, but place it inside the main worktree instead of alongside it as would ordinarily be preferable:

git worktree add mybranch

Observe that, even though git status reports it as untracked, git clean will not remove it, and that, for git clean, the reason appears to be that it regards it as a nested repository (optionally use -f instead of -n):

git status
git clean -dn
git clean -xdn
git status
git add .
git status

Unstage and check that the situation is back to where it was when the worktree was added:

git rm --cached mybranch
git worktree list
git status

See what gix clean would do, and that it really does it, deleting the mybranch subdirectory that belonged to a git worktree managed working tree:

gix clean -n
gix clean -rn
gix clean -re
git status
git worktree list
ls mybranch

With -r and not -x or -d – with output

Here's all that, with output:

ek@Glub:~/src$ git init with-worktree
Initialized empty Git repository in /home/ek/src/with-worktree/.git/
ek@Glub:~/src$ cd with-worktree
ek@Glub:~/src/with-worktree (main #)$ touch file
ek@Glub:~/src/with-worktree (main #%)$ git add .
ek@Glub:~/src/with-worktree (main +)$ git commit -m 'Initial commit'
[main (root-commit) be9dc23] Initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file
ek@Glub:~/src/with-worktree (main)$ git worktree add mybranch
Preparing worktree (new branch 'mybranch')
HEAD is now at be9dc23 Initial commit
ek@Glub:~/src/with-worktree (main %)$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        mybranch/

nothing added to commit but untracked files present (use "git add" to track)
ek@Glub:~/src/with-worktree (main %)$ git clean -dn
ek@Glub:~/src/with-worktree (main %)$ git clean -xdn
ek@Glub:~/src/with-worktree (main %)$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        mybranch/

nothing added to commit but untracked files present (use "git add" to track)
ek@Glub:~/src/with-worktree (main %)$ git add .
warning: adding embedded git repository: mybranch
hint: You've added another git repository inside your current repository.
hint: Clones of the outer repository will not contain the contents of
hint: the embedded repository and will not know how to obtain it.
hint: If you meant to add a submodule, use:
hint:
hint:   git submodule add <url> mybranch
hint:
hint: If you added this path by mistake, you can remove it from the
hint: index with:
hint:
hint:   git rm --cached mybranch
hint:
hint: See "git help submodule" for more information.
ek@Glub:~/src/with-worktree (main +)$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   mybranch
ek@Glub:~/src/with-worktree (main +)$ git rm --cached mybranch
rm 'mybranch'
ek@Glub:~/src/with-worktree (main %)$ git worktree list
/home/ek/src/with-worktree           be9dc23 [main]
/home/ek/src/with-worktree/mybranch  be9dc23 [mybranch]
ek@Glub:~/src/with-worktree (main %)$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        mybranch/

nothing added to commit but untracked files present (use "git add" to track)
ek@Glub:~/src/with-worktree (main %)$ gix clean -n
Nothing to clean (Skipped 1 repository - show with -r)
ek@Glub:~/src/with-worktree (main %)$ gix clean -rn
WOULD remove repository mybranch/
ek@Glub:~/src/with-worktree (main %)$ gix clean -re
removing repository mybranch/
ek@Glub:~/src/with-worktree (main)$ git status
On branch main
nothing to commit, working tree clean
ek@Glub:~/src/with-worktree (main)$ git worktree list
/home/ek/src/with-worktree           be9dc23 [main]
/home/ek/src/with-worktree/mybranch  be9dc23 [mybranch] prunable
ek@Glub:~/src/with-worktree (main)$ ls mybranch
ls: cannot access 'mybranch': No such file or directory

Without -r, and with -x and -d, when .gitignore has * – instructions

This can be done as a variation on the above example, for which separate explication may not be required. This instead produces it on a real-world repository that lists * followed by ! exclusions--the cargo-update repository that was also used as an example in #1458.

Get the repository and observe what it ignores:

git clone https://github.com/nabijaczleweli/cargo-update.git
cd cargo-update
cat .gitignore

Observe that #1458 is fixed (#1462):

gix clean -xdn
gix clean -xde

Create a nested worktree:

git worktree add mybranch
git worktree list

Verify that git clean does not delete it, even when told to remove directories and ignored entries:

git clean -xdn
git clean -xdf
ls -ld mybranch

Use a gix clean command to delete it, even though this is expected to delete some nested repositories but is intended not to delete any of our worktrees, and verify that it is gone:

gix clean -xdn
gix clean -xde
ls -ld mybranch

Without -r, and with -x and -d, when .gitignore has * – output

Here's all that, with output:

ek@Glub:~/src$ git clone https://github.com/nabijaczleweli/cargo-update.git
Cloning into 'cargo-update'...
remote: Enumerating objects: 328251, done.
remote: Counting objects: 100% (83271/83271), done.
remote: Compressing objects: 100% (1630/1630), done.
remote: Total 328251 (delta 81523), reused 83220 (delta 81477), pack-reused 244980
Receiving objects: 100% (328251/328251), 110.53 MiB | 35.35 MiB/s, done.
Resolving deltas: 100% (320324/320324), done.
ek@Glub:~/src$ cd cargo-update
ek@Glub:~/src/cargo-update (master=)$ cat .gitignore
*
!.gitignore
!.travis.yml
!gh_rsa.enc
!appveyor.yml
!LICENSE
!Cargo.toml
!rustfmt.toml
!build.rs
!cargo-install-update-manifest.rc
!cargo-install-update.exe.manifest
!*.sublime-project
!*.md
!.github
!.github/**
!src
!src/**
!man
!man/**
!tests
!tests/**
!test-data
!test-data/**
ek@Glub:~/src/cargo-update (master=)$ gix clean -xdn
Nothing to clean
ek@Glub:~/src/cargo-update (master=)$ gix clean -xde
ek@Glub:~/src/cargo-update (master=)$
ek@Glub:~/src/cargo-update (master=)$ git worktree add mybranch
Preparing worktree (new branch 'mybranch')
HEAD is now at 6334da9b99 they should invent rustfmt that works imo
ek@Glub:~/src/cargo-update (master=)$ git worktree list
/home/ek/src/cargo-update           6334da9b99 [master]
/home/ek/src/cargo-update/mybranch  6334da9b99 [mybranch]
ek@Glub:~/src/cargo-update (master=)$ git clean -xdn
ek@Glub:~/src/cargo-update (master=)$ git clean -xdf
ek@Glub:~/src/cargo-update (master=)$ ls -ld mybranch
drwxr-xr-x 7 ek ek 4096 Jul 23 16:39 mybranch
ek@Glub:~/src/cargo-update (master=)$ gix clean -xdn
WOULD remove mybranch/ (🗑️)

WARNING: would remove repositories hidden inside ignored directories - use --skip-hidden-repositories to skip
ek@Glub:~/src/cargo-update (master=)$ gix clean -xde
removing mybranch/ (🗑️)
ek@Glub:~/src/cargo-update (master=)$ ls -ld mybranch
ls: cannot access 'mybranch': No such file or directory

Metadata

Metadata

Assignees

Labels

acknowledgedan issue is accepted as shortcoming to be fixed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions