Merge branch 'es/worktree-forced-ops-fix'
Fix a bug in which the same path could be registered under multiple worktree entries if the path was missing (for instance, was removed manually). Also, as a convenience, expand the number of cases in which --force is applicable. * es/worktree-forced-ops-fix: doc-diff: force worktree add worktree: delete .git/worktrees if empty after 'remove' worktree: teach 'remove' to override lock when --force given twice worktree: teach 'move' to override lock when --force given twice worktree: teach 'add' to respect --force for registered but missing path worktree: disallow adding same path multiple times worktree: prepare for more checks of whether path can become worktree worktree: generalize delete_git_dir() to reduce code duplication worktree: move delete_git_dir() earlier in file for upcoming new callers worktree: don't die() in library function find_worktree()maint
						commit
						1c515bf7e2
					
				|  | @ -75,7 +75,7 @@ fi | ||||||
| # results that don't differ between the two trees. | # results that don't differ between the two trees. | ||||||
| if ! test -d "$tmp/worktree" | if ! test -d "$tmp/worktree" | ||||||
| then | then | ||||||
| 	git worktree add --detach "$tmp/worktree" "$from" && | 	git worktree add -f --detach "$tmp/worktree" "$from" && | ||||||
| 	dots=$(echo "$tmp/worktree" | sed 's#[^/]*#..#g') && | 	dots=$(echo "$tmp/worktree" | sed 's#[^/]*#..#g') && | ||||||
| 	ln -s "$dots/config.mak" "$tmp/worktree/config.mak" | 	ln -s "$dots/config.mak" "$tmp/worktree/config.mak" | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | @ -120,8 +120,16 @@ OPTIONS | ||||||
| --force:: | --force:: | ||||||
| 	By default, `add` refuses to create a new working tree when | 	By default, `add` refuses to create a new working tree when | ||||||
| 	`<commit-ish>` is a branch name and is already checked out by | 	`<commit-ish>` is a branch name and is already checked out by | ||||||
| 	another working tree and `remove` refuses to remove an unclean | 	another working tree, or if `<path>` is already assigned to some | ||||||
| 	working tree. This option overrides these safeguards. | 	working tree but is missing (for instance, if `<path>` was deleted | ||||||
|  | 	manually). This option overrides these safeguards. To add a missing but | ||||||
|  | 	locked working tree path, specify `--force` twice. | ||||||
|  | + | ||||||
|  | `move` refuses to move a locked working tree unless `--force` is specified | ||||||
|  | twice. | ||||||
|  | + | ||||||
|  | `remove` refuses to remove an unclean working tree unless `--force` is used. | ||||||
|  | To remove a locked working tree, specify `--force` twice. | ||||||
|  |  | ||||||
| -b <new-branch>:: | -b <new-branch>:: | ||||||
| -B <new-branch>:: | -B <new-branch>:: | ||||||
|  |  | ||||||
|  | @ -47,6 +47,26 @@ static int git_worktree_config(const char *var, const char *value, void *cb) | ||||||
| 	return git_default_config(var, value, cb); | 	return git_default_config(var, value, cb); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | static int delete_git_dir(const char *id) | ||||||
|  | { | ||||||
|  | 	struct strbuf sb = STRBUF_INIT; | ||||||
|  | 	int ret; | ||||||
|  |  | ||||||
|  | 	strbuf_addstr(&sb, git_common_path("worktrees/%s", id)); | ||||||
|  | 	ret = remove_dir_recursively(&sb, 0); | ||||||
|  | 	if (ret < 0 && errno == ENOTDIR) | ||||||
|  | 		ret = unlink(sb.buf); | ||||||
|  | 	if (ret) | ||||||
|  | 		error_errno(_("failed to delete '%s'"), sb.buf); | ||||||
|  | 	strbuf_release(&sb); | ||||||
|  | 	return ret; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void delete_worktrees_dir_if_empty(void) | ||||||
|  | { | ||||||
|  | 	rmdir(git_path("worktrees")); /* ignore failed removal */ | ||||||
|  | } | ||||||
|  |  | ||||||
| static int prune_worktree(const char *id, struct strbuf *reason) | static int prune_worktree(const char *id, struct strbuf *reason) | ||||||
| { | { | ||||||
| 	struct stat st; | 	struct stat st; | ||||||
|  | @ -116,10 +136,8 @@ static int prune_worktree(const char *id, struct strbuf *reason) | ||||||
| static void prune_worktrees(void) | static void prune_worktrees(void) | ||||||
| { | { | ||||||
| 	struct strbuf reason = STRBUF_INIT; | 	struct strbuf reason = STRBUF_INIT; | ||||||
| 	struct strbuf path = STRBUF_INIT; |  | ||||||
| 	DIR *dir = opendir(git_path("worktrees")); | 	DIR *dir = opendir(git_path("worktrees")); | ||||||
| 	struct dirent *d; | 	struct dirent *d; | ||||||
| 	int ret; |  | ||||||
| 	if (!dir) | 	if (!dir) | ||||||
| 		return; | 		return; | ||||||
| 	while ((d = readdir(dir)) != NULL) { | 	while ((d = readdir(dir)) != NULL) { | ||||||
|  | @ -132,18 +150,12 @@ static void prune_worktrees(void) | ||||||
| 			printf("%s\n", reason.buf); | 			printf("%s\n", reason.buf); | ||||||
| 		if (show_only) | 		if (show_only) | ||||||
| 			continue; | 			continue; | ||||||
| 		git_path_buf(&path, "worktrees/%s", d->d_name); | 		delete_git_dir(d->d_name); | ||||||
| 		ret = remove_dir_recursively(&path, 0); |  | ||||||
| 		if (ret < 0 && errno == ENOTDIR) |  | ||||||
| 			ret = unlink(path.buf); |  | ||||||
| 		if (ret) |  | ||||||
| 			error_errno(_("failed to remove '%s'"), path.buf); |  | ||||||
| 	} | 	} | ||||||
| 	closedir(dir); | 	closedir(dir); | ||||||
| 	if (!show_only) | 	if (!show_only) | ||||||
| 		rmdir(git_path("worktrees")); | 		delete_worktrees_dir_if_empty(); | ||||||
| 	strbuf_release(&reason); | 	strbuf_release(&reason); | ||||||
| 	strbuf_release(&path); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| static int prune(int ac, const char **av, const char *prefix) | static int prune(int ac, const char **av, const char *prefix) | ||||||
|  | @ -212,6 +224,43 @@ static const char *worktree_basename(const char *path, int *olen) | ||||||
| 	return name; | 	return name; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | static void validate_worktree_add(const char *path, const struct add_opts *opts) | ||||||
|  | { | ||||||
|  | 	struct worktree **worktrees; | ||||||
|  | 	struct worktree *wt; | ||||||
|  | 	int locked; | ||||||
|  |  | ||||||
|  | 	if (file_exists(path) && !is_empty_dir(path)) | ||||||
|  | 		die(_("'%s' already exists"), path); | ||||||
|  |  | ||||||
|  | 	worktrees = get_worktrees(0); | ||||||
|  | 	/* | ||||||
|  | 	 * find_worktree()'s suffix matching may undesirably find the main | ||||||
|  | 	 * rather than a linked worktree (for instance, when the basenames | ||||||
|  | 	 * of the main worktree and the one being created are the same). | ||||||
|  | 	 * We're only interested in linked worktrees, so skip the main | ||||||
|  | 	 * worktree with +1. | ||||||
|  | 	 */ | ||||||
|  | 	wt = find_worktree(worktrees + 1, NULL, path); | ||||||
|  | 	if (!wt) | ||||||
|  | 		goto done; | ||||||
|  |  | ||||||
|  | 	locked = !!is_worktree_locked(wt); | ||||||
|  | 	if ((!locked && opts->force) || (locked && opts->force > 1)) { | ||||||
|  | 		if (delete_git_dir(wt->id)) | ||||||
|  | 		    die(_("unable to re-add worktree '%s'"), path); | ||||||
|  | 		goto done; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (locked) | ||||||
|  | 		die(_("'%s' is a missing but locked worktree;\nuse 'add -f -f' to override, or 'unlock' and 'prune' or 'remove' to clear"), path); | ||||||
|  | 	else | ||||||
|  | 		die(_("'%s' is a missing but already registered worktree;\nuse 'add -f' to override, or 'prune' or 'remove' to clear"), path); | ||||||
|  |  | ||||||
|  | done: | ||||||
|  | 	free_worktrees(worktrees); | ||||||
|  | } | ||||||
|  |  | ||||||
| static int add_worktree(const char *path, const char *refname, | static int add_worktree(const char *path, const char *refname, | ||||||
| 			const struct add_opts *opts) | 			const struct add_opts *opts) | ||||||
| { | { | ||||||
|  | @ -226,8 +275,7 @@ static int add_worktree(const char *path, const char *refname, | ||||||
| 	struct commit *commit = NULL; | 	struct commit *commit = NULL; | ||||||
| 	int is_branch = 0; | 	int is_branch = 0; | ||||||
|  |  | ||||||
| 	if (file_exists(path) && !is_empty_dir(path)) | 	validate_worktree_add(path, opts); | ||||||
| 		die(_("'%s' already exists"), path); |  | ||||||
|  |  | ||||||
| 	/* is 'refname' a branch or commit? */ | 	/* is 'refname' a branch or commit? */ | ||||||
| 	if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && | 	if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && | ||||||
|  | @ -697,13 +745,17 @@ static void validate_no_submodules(const struct worktree *wt) | ||||||
|  |  | ||||||
| static int move_worktree(int ac, const char **av, const char *prefix) | static int move_worktree(int ac, const char **av, const char *prefix) | ||||||
| { | { | ||||||
|  | 	int force = 0; | ||||||
| 	struct option options[] = { | 	struct option options[] = { | ||||||
|  | 		OPT__FORCE(&force, | ||||||
|  | 			 N_("force move even if worktree is dirty or locked"), | ||||||
|  | 			 PARSE_OPT_NOCOMPLETE), | ||||||
| 		OPT_END() | 		OPT_END() | ||||||
| 	}; | 	}; | ||||||
| 	struct worktree **worktrees, *wt; | 	struct worktree **worktrees, *wt; | ||||||
| 	struct strbuf dst = STRBUF_INIT; | 	struct strbuf dst = STRBUF_INIT; | ||||||
| 	struct strbuf errmsg = STRBUF_INIT; | 	struct strbuf errmsg = STRBUF_INIT; | ||||||
| 	const char *reason; | 	const char *reason = NULL; | ||||||
| 	char *path; | 	char *path; | ||||||
|  |  | ||||||
| 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0); | 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0); | ||||||
|  | @ -734,12 +786,13 @@ static int move_worktree(int ac, const char **av, const char *prefix) | ||||||
|  |  | ||||||
| 	validate_no_submodules(wt); | 	validate_no_submodules(wt); | ||||||
|  |  | ||||||
|  | 	if (force < 2) | ||||||
| 		reason = is_worktree_locked(wt); | 		reason = is_worktree_locked(wt); | ||||||
| 	if (reason) { | 	if (reason) { | ||||||
| 		if (*reason) | 		if (*reason) | ||||||
| 			die(_("cannot move a locked working tree, lock reason: %s"), | 			die(_("cannot move a locked working tree, lock reason: %s\nuse 'move -f -f' to override or unlock first"), | ||||||
| 			    reason); | 			    reason); | ||||||
| 		die(_("cannot move a locked working tree")); | 		die(_("cannot move a locked working tree;\nuse 'move -f -f' to override or unlock first")); | ||||||
| 	} | 	} | ||||||
| 	if (validate_worktree(wt, &errmsg, 0)) | 	if (validate_worktree(wt, &errmsg, 0)) | ||||||
| 		die(_("validation failed, cannot move working tree: %s"), | 		die(_("validation failed, cannot move working tree: %s"), | ||||||
|  | @ -822,32 +875,18 @@ static int delete_git_work_tree(struct worktree *wt) | ||||||
| 	return ret; | 	return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| static int delete_git_dir(struct worktree *wt) |  | ||||||
| { |  | ||||||
| 	struct strbuf sb = STRBUF_INIT; |  | ||||||
| 	int ret = 0; |  | ||||||
|  |  | ||||||
| 	strbuf_addstr(&sb, git_common_path("worktrees/%s", wt->id)); |  | ||||||
| 	if (remove_dir_recursively(&sb, 0)) { |  | ||||||
| 		error_errno(_("failed to delete '%s'"), sb.buf); |  | ||||||
| 		ret = -1; |  | ||||||
| 	} |  | ||||||
| 	strbuf_release(&sb); |  | ||||||
| 	return ret; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| static int remove_worktree(int ac, const char **av, const char *prefix) | static int remove_worktree(int ac, const char **av, const char *prefix) | ||||||
| { | { | ||||||
| 	int force = 0; | 	int force = 0; | ||||||
| 	struct option options[] = { | 	struct option options[] = { | ||||||
| 		OPT__FORCE(&force, | 		OPT__FORCE(&force, | ||||||
| 			 N_("force removing even if the worktree is dirty"), | 			 N_("force removal even if worktree is dirty or locked"), | ||||||
| 			 PARSE_OPT_NOCOMPLETE), | 			 PARSE_OPT_NOCOMPLETE), | ||||||
| 		OPT_END() | 		OPT_END() | ||||||
| 	}; | 	}; | ||||||
| 	struct worktree **worktrees, *wt; | 	struct worktree **worktrees, *wt; | ||||||
| 	struct strbuf errmsg = STRBUF_INIT; | 	struct strbuf errmsg = STRBUF_INIT; | ||||||
| 	const char *reason; | 	const char *reason = NULL; | ||||||
| 	int ret = 0; | 	int ret = 0; | ||||||
|  |  | ||||||
| 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0); | 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0); | ||||||
|  | @ -860,12 +899,13 @@ static int remove_worktree(int ac, const char **av, const char *prefix) | ||||||
| 		die(_("'%s' is not a working tree"), av[0]); | 		die(_("'%s' is not a working tree"), av[0]); | ||||||
| 	if (is_main_worktree(wt)) | 	if (is_main_worktree(wt)) | ||||||
| 		die(_("'%s' is a main working tree"), av[0]); | 		die(_("'%s' is a main working tree"), av[0]); | ||||||
|  | 	if (force < 2) | ||||||
| 		reason = is_worktree_locked(wt); | 		reason = is_worktree_locked(wt); | ||||||
| 	if (reason) { | 	if (reason) { | ||||||
| 		if (*reason) | 		if (*reason) | ||||||
| 			die(_("cannot remove a locked working tree, lock reason: %s"), | 			die(_("cannot remove a locked working tree, lock reason: %s\nuse 'remove -f -f' to override or unlock first"), | ||||||
| 			    reason); | 			    reason); | ||||||
| 		die(_("cannot remove a locked working tree")); | 		die(_("cannot remove a locked working tree;\nuse 'remove -f -f' to override or unlock first")); | ||||||
| 	} | 	} | ||||||
| 	if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK)) | 	if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK)) | ||||||
| 		die(_("validation failed, cannot remove working tree: %s"), | 		die(_("validation failed, cannot remove working tree: %s"), | ||||||
|  | @ -882,7 +922,8 @@ static int remove_worktree(int ac, const char **av, const char *prefix) | ||||||
| 	 * continue on even if ret is non-zero, there's no going back | 	 * continue on even if ret is non-zero, there's no going back | ||||||
| 	 * from here. | 	 * from here. | ||||||
| 	 */ | 	 */ | ||||||
| 	ret |= delete_git_dir(wt); | 	ret |= delete_git_dir(wt->id); | ||||||
|  | 	delete_worktrees_dir_if_empty(); | ||||||
|  |  | ||||||
| 	free_worktrees(worktrees); | 	free_worktrees(worktrees); | ||||||
| 	return ret; | 	return ret; | ||||||
|  |  | ||||||
|  | @ -552,4 +552,22 @@ test_expect_success '"add" in bare repo invokes post-checkout hook' ' | ||||||
| 	test_cmp hook.expect goozy/hook.actual | 	test_cmp hook.expect goozy/hook.actual | ||||||
| ' | ' | ||||||
|  |  | ||||||
|  | test_expect_success '"add" an existing but missing worktree' ' | ||||||
|  | 	git worktree add --detach pneu && | ||||||
|  | 	test_must_fail git worktree add --detach pneu && | ||||||
|  | 	rm -fr pneu && | ||||||
|  | 	test_must_fail git worktree add --detach pneu && | ||||||
|  | 	git worktree add --force --detach pneu | ||||||
|  | ' | ||||||
|  |  | ||||||
|  | test_expect_success '"add" an existing locked but missing worktree' ' | ||||||
|  | 	git worktree add --detach gnoo && | ||||||
|  | 	git worktree lock gnoo && | ||||||
|  | 	test_when_finished "git worktree unlock gnoo || :" && | ||||||
|  | 	rm -fr gnoo && | ||||||
|  | 	test_must_fail git worktree add --detach gnoo && | ||||||
|  | 	test_must_fail git worktree add --force --detach gnoo && | ||||||
|  | 	git worktree add --force --force --detach gnoo | ||||||
|  | ' | ||||||
|  |  | ||||||
| test_done | test_done | ||||||
|  |  | ||||||
|  | @ -98,6 +98,20 @@ test_expect_success 'move worktree to another dir' ' | ||||||
| 	test_cmp expected2 actual2 | 	test_cmp expected2 actual2 | ||||||
| ' | ' | ||||||
|  |  | ||||||
|  | test_expect_success 'move locked worktree (force)' ' | ||||||
|  | 	test_when_finished " | ||||||
|  | 		git worktree unlock flump || : | ||||||
|  | 		git worktree remove flump || : | ||||||
|  | 		git worktree unlock ploof || : | ||||||
|  | 		git worktree remove ploof || : | ||||||
|  | 		" && | ||||||
|  | 	git worktree add --detach flump && | ||||||
|  | 	git worktree lock flump && | ||||||
|  | 	test_must_fail git worktree move flump ploof" && | ||||||
|  | 	test_must_fail git worktree move --force flump ploof" && | ||||||
|  | 	git worktree move --force --force flump ploof | ||||||
|  | ' | ||||||
|  |  | ||||||
| test_expect_success 'remove main worktree' ' | test_expect_success 'remove main worktree' ' | ||||||
| 	test_must_fail git worktree remove . | 	test_must_fail git worktree remove . | ||||||
| ' | ' | ||||||
|  | @ -141,4 +155,34 @@ test_expect_success 'NOT remove missing-but-locked worktree' ' | ||||||
| 	test_path_is_dir .git/worktrees/gone-but-locked | 	test_path_is_dir .git/worktrees/gone-but-locked | ||||||
| ' | ' | ||||||
|  |  | ||||||
|  | test_expect_success 'proper error when worktree not found' ' | ||||||
|  | 	for i in noodle noodle/bork | ||||||
|  | 	do | ||||||
|  | 		test_must_fail git worktree lock $i 2>err && | ||||||
|  | 		test_i18ngrep "not a working tree" err || return 1 | ||||||
|  | 	done | ||||||
|  | ' | ||||||
|  |  | ||||||
|  | test_expect_success 'remove locked worktree (force)' ' | ||||||
|  | 	git worktree add --detach gumby && | ||||||
|  | 	test_when_finished "git worktree remove gumby || :" && | ||||||
|  | 	git worktree lock gumby && | ||||||
|  | 	test_when_finished "git worktree unlock gumby || :" && | ||||||
|  | 	test_must_fail git worktree remove gumby && | ||||||
|  | 	test_must_fail git worktree remove --force gumby && | ||||||
|  | 	git worktree remove --force --force gumby | ||||||
|  | ' | ||||||
|  |  | ||||||
|  | test_expect_success 'remove cleans up .git/worktrees when empty' ' | ||||||
|  | 	git init moog && | ||||||
|  | 	( | ||||||
|  | 		cd moog && | ||||||
|  | 		test_commit bim && | ||||||
|  | 		git worktree add --detach goom && | ||||||
|  | 		test_path_exists .git/worktrees && | ||||||
|  | 		git worktree remove goom && | ||||||
|  | 		test_path_is_missing .git/worktrees | ||||||
|  | 	) | ||||||
|  | ' | ||||||
|  |  | ||||||
| test_done | test_done | ||||||
|  |  | ||||||
|  | @ -217,7 +217,11 @@ struct worktree *find_worktree(struct worktree **list, | ||||||
|  |  | ||||||
| 	if (prefix) | 	if (prefix) | ||||||
| 		arg = to_free = prefix_filename(prefix, arg); | 		arg = to_free = prefix_filename(prefix, arg); | ||||||
| 	path = real_pathdup(arg, 1); | 	path = real_pathdup(arg, 0); | ||||||
|  | 	if (!path) { | ||||||
|  | 		free(to_free); | ||||||
|  | 		return NULL; | ||||||
|  | 	} | ||||||
| 	for (; *list; list++) | 	for (; *list; list++) | ||||||
| 		if (!fspathcmp(path, real_path((*list)->path))) | 		if (!fspathcmp(path, real_path((*list)->path))) | ||||||
| 			break; | 			break; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Junio C Hamano
						Junio C Hamano