git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 1/1] worktree: delete branches auto-created by 'worktree add'

From
Pratyush Yadav <me@yadavpratyush.com>
Date
Dec 14, 2019, 16:14 UTC
Message-ID
<20191214161438.16157-2-me@yadavpratyush.com>
In-Reply-To
<20191214161438.16157-1-me@yadavpratyush.com>

When no branch name is supplied to 'worktree add', it creates a new branch based on the name of the directory the new worktree is located in. But when the worktree is later removed, that created branch is left over.

Remove that branch when removing the worktree. To make sure no commits are lost, the branch won't be deleted if it has moved.

An example use case of when something like this is useful is when the user wants to check out a separate worktree to run and test on an older version, but don't want to touch the current worktree. So, they create a worktree, run some tests, and then remove it. But this leaves behind a branch the user never created in the first place.

So, remove the branch if nothing was done on it.
Signed-off-by: Pratyush Yadav <me@yadavpratyush.com>
---
 Documentation/git-worktree.txt |  9 ++++--
 builtin/worktree.c             | 52 ++++++++++++++++++++++++++++++++--
 t/t2403-worktree-move.sh       | 45 +++++++++++++++++++++++++++++
 3 files changed, 101 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 85d92c9761..87b84be608 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -73,8 +73,9 @@ If `<commit-ish>` is omitted and neither `-b` nor `-B` nor `--detach` used,
 then, as a convenience, the new worktree is associated with a branch
 (call it `<branch>`) named after `$(basename <path>)`.  If `<branch>`
 doesn't exist, a new branch based on HEAD is automatically created as
-if `-b <branch>` was given.  If `<branch>` does exist, it will be
-checked out in the new worktree, if it's not checked out anywhere
+if `-b <branch>` was given.  In this case, if `<branch>` is not moved, it is
+automatically deleted when the worktree is removed.  If `<branch>` does exist,
+it will be checked out in the new worktree, if it's not checked out anywhere
 else, otherwise the command will refuse to create the worktree (unless
 `--force` is used).
 
@@ -108,6 +109,10 @@ Remove a working tree. Only clean working trees (no untracked files
 and no modification in tracked files) can be removed. Unclean working
 trees or ones with submodules can be removed with `--force`. The main
 working tree cannot be removed.
++
+Removing a working tree might lead to its associated branch being deleted if
+it was auto-created and has not moved since. See `add` for more information on
+when exactly this can happen.
 
 unlock::
 
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d6bc5263f1..c62811259a 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -35,6 +35,7 @@ struct add_opts {
 static int show_only;
 static int verbose;
 static int guess_remote;
+static int auto_create;
 static timestamp_t expire;
 
 static int git_worktree_config(const char *var, const char *value, void *cb)
@@ -270,11 +271,13 @@ static int add_worktree(const char *path, const char *refname,
 	struct child_process cp = CHILD_PROCESS_INIT;
 	struct argv_array child_env = ARGV_ARRAY_INIT;
 	unsigned int counter = 0;
-	int len, ret;
+	int len, ret, fd;
 	struct strbuf symref = STRBUF_INIT;
 	struct commit *commit = NULL;
 	int is_branch = 0;
 	struct strbuf sb_name = STRBUF_INIT;
+	struct object_id oid;
+	char *hex;
 
 	validate_worktree_add(path, opts);
 
@@ -353,6 +356,18 @@ static int add_worktree(const char *path, const char *refname,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
 	write_file(sb.buf, "../..");
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/auto_created", sb_repo.buf);
+	/* Mark this branch as an "auto-created" one. */
+	if (auto_create) {
+		fd = xopen(sb.buf, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+		get_oid("HEAD", &oid);
+		hex = oid_to_hex(&oid);
+		write_file_buf(sb.buf, hex, strlen(hex));
+
+		if (close(fd))
+			die(_("could not close '%s'"), sb.buf);
+	}
 
 	argv_array_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf);
 	argv_array_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
@@ -576,6 +591,8 @@ static int add(int ac, const char **av, const char *prefix)
 		if (run_command(&cp))
 			return -1;
 		branch = new_branch;
+
+		auto_create = 1;
 	} else if (opt_track) {
 		die(_("--[no-]track can only be used if a new branch is created"));
 	}
@@ -912,9 +929,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
 		OPT_END()
 	};
 	struct worktree **worktrees, *wt;
-	struct strbuf errmsg = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT, sb = STRBUF_INIT, hex = STRBUF_INIT;
 	const char *reason = NULL;
-	int ret = 0;
+	int ret = 0, delete_auto_created = 0;
+	struct object_id oid;
 
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac != 1)
@@ -939,6 +957,23 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
 		    errmsg.buf);
 	strbuf_release(&errmsg);
 
+	/*
+	 * Check if we auto-created a branch for this worktree and it hasn't
+	 * moved since. Do it before the contents of the worktree get wiped.
+	 * Delete the branch later because it is checked out right now.
+	 */
+	git_path_buf(&sb, "worktrees/%s/auto_created", wt->id);
+	if (file_exists(sb.buf)) {
+		strbuf_read_file(&hex, sb.buf, 0);
+		get_oid(wt->id, &oid);
+
+		if (strcmp(hex.buf, oid_to_hex(&oid)) == 0)
+			delete_auto_created = 1;
+	}
+
+	strbuf_release(&sb);
+	strbuf_release(&hex);
+
 	if (file_exists(wt->path)) {
 		if (!force)
 			check_clean_worktree(wt, av[0]);
@@ -952,6 +987,17 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
 	ret |= delete_git_dir(wt->id);
 	delete_worktrees_dir_if_empty();
 
+	if (delete_auto_created) {
+		struct child_process cp = CHILD_PROCESS_INIT;
+		cp.git_cmd = 1;
+
+		argv_array_push(&cp.args, "branch");
+		argv_array_push(&cp.args, "-d");
+		argv_array_push(&cp.args, wt->id);
+
+		ret |= run_command(&cp);
+	}
+
 	free_worktrees(worktrees);
 	return ret;
 }
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 939d18d728..c71c0bc1c7 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -222,4 +222,49 @@ test_expect_success 'not remove a repo with initialized submodule' '
 	)
 '
 
+test_expect_success 'remove auto-created branch' '
+	(
+		git worktree add to-remove &&
+		git worktree remove to-remove &&
+		git branch -l to-remove >branch_list &&
+		test_line_count = 0 branch_list
+	)
+'
+
+test_expect_success 'do not remove a branch that was not auto-created' '
+	(
+		git worktree add -b new_branch to-remove &&
+		git worktree remove to-remove &&
+		git branch -l new_branch >branch_list &&
+		test_line_count = 1 branch_list &&
+		git branch -d new_branch &&
+		git branch foo &&
+		git worktree add to-remove foo &&
+		git worktree remove to-remove &&
+		git branch -l foo >branch_list &&
+		test_line_count = 1 branch_list &&
+		git branch -d foo &&
+		git branch to-remove &&
+		git worktree add to-remove &&
+		git worktree remove to-remove &&
+		git branch -l to-remove >branch_list &&
+		test_line_count = 1 branch_list &&
+		git branch -d to-remove
+	)
+'
+
+test_expect_success 'do not remove auto-created branch that was moved' '
+	(
+		git worktree add to-remove &&
+		cd to-remove &&
+		test_commit foo &&
+		cd ../ &&
+		git worktree remove to-remove &&
+		git branch -l to-remove >branch_list &&
+		test_line_count = 1 branch_list &&
+		git branch -D to-remove
+	)
+'
+
+
 test_done
-- 
2.24.1
Previous: Pratyush YadavNext: Pratyush Yadav
Message 2 of 10 in “worktree: delete branches auto-created by 'worktree add'”
  1. 0/1 worktree: delete branches auto-created by 'worktree add'Pratyush Yadav, Dec 14, 2019
  2. 1/1 worktree: delete branches auto-created by 'worktree add'Pratyush Yadav, Dec 14, 2019
  3. Pratyush YadavDec 18, 2019
  4. Eric SunshineDec 18, 2019
  5. Eric SunshineDec 27, 2019
  6. Pratyush YadavJan 4, 2020
  7. Eric SunshineJan 5, 2020
  8. Eric SunshineJan 6, 2020
  9. Pratyush YadavJan 6, 2020
  10. Eric SunshineJan 9, 2020

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.