From: Junio C Hamano Date: Wed, 14 Mar 2018 19:01:05 +0000 (-0700) Subject: Merge branch 'nd/worktree-move' X-Git-Tag: v2.17.0-rc0~11 X-Git-Url: https://git.lorimer.id.au/gitweb.git/diff_plain/bd0f794342d5b3921f2d5d4bffce87ec7b7e4d96?ds=inline;hp=-c Merge branch 'nd/worktree-move' "git worktree" learned move and remove subcommands. * nd/worktree-move: t2028: fix minor error and issues in newly-added "worktree move" tests worktree remove: allow it when $GIT_WORK_TREE is already gone worktree remove: new command worktree move: refuse to move worktrees with submodules worktree move: accept destination as directory worktree move: new command worktree.c: add update_worktree_location() worktree.c: add validate_worktree() --- bd0f794342d5b3921f2d5d4bffce87ec7b7e4d96 diff --combined Documentation/git-worktree.txt index 5ac3f68ab5,d322acbc67..e7eb24ab85 --- a/Documentation/git-worktree.txt +++ b/Documentation/git-worktree.txt @@@ -12,7 -12,9 +12,9 @@@ SYNOPSI 'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b ] [] 'git worktree list' [--porcelain] 'git worktree lock' [--reason ] + 'git worktree move' 'git worktree prune' [-n] [-v] [--expire ] + 'git worktree remove' [--force] 'git worktree unlock' DESCRIPTION @@@ -34,10 -36,6 +36,6 @@@ The working tree's administrative file `git worktree prune` in the main or any linked working tree to clean up any stale administrative files. - If you move a linked working tree, you need to manually update the - administrative files so that they do not get pruned automatically. See - section "DETAILS" for more information. - If a linked working tree is stored on a portable device or network share which is not always mounted, you can prevent its administrative files from being pruned by issuing the `git worktree lock` command, optionally @@@ -52,11 -50,10 +50,11 @@@ is linked to the current repository, sh directory specific files such as HEAD, index, etc. `-` may also be specified as ``; it is synonymous with `@{-1}`. + -If is a branch name (call it `` and is not found, +If is a branch name (call it ``) and is not found, and neither `-b` nor `-B` nor `--detach` are used, but there does exist a tracking branch in exactly one remote (call it ``) -with a matching name, treat as equivalent to +with a matching name, treat as equivalent to: ++ ------------ $ git worktree add --track -b / ------------ @@@ -80,10 -77,22 +78,22 @@@ files from being pruned automatically. being moved or deleted. Optionally, specify a reason for the lock with `--reason`. + move:: + + Move a working tree to a new location. Note that the main working tree + or linked working trees containing submodules cannot be moved. + prune:: Prune working tree information in $GIT_DIR/worktrees. + remove:: + + 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. + unlock:: Unlock a working tree, allowing it to be pruned, moved or deleted. @@@ -93,9 -102,10 +103,10 @@@ OPTION -f:: --force:: - By default, `add` refuses to create a new working tree when `` is a branch name and - is already checked out by another working tree. This option overrides - that safeguard. + By default, `add` refuses to create a new working tree when + `` is a branch name and is already checked out by + another working tree and `remove` refuses to remove an unclean + working tree. This option overrides that safeguard. -b :: -B :: @@@ -197,7 -207,7 +208,7 @@@ thumb is do not make any assumption abo $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path. - If you move a linked working tree, you need to update the 'gitdir' file + If you manually move a linked working tree, you need to update the 'gitdir' file in the entry's directory. For example, if a linked working tree is moved to `/newpath/test-next` and its `.git` file points to `/path/main/.git/worktrees/test-next`, then update @@@ -277,13 -287,6 +288,6 @@@ Multiple checkout in general is still e for submodules is incomplete. It is NOT recommended to make multiple checkouts of a superproject. - git-worktree could provide more automation for tasks currently - performed manually, such as: - - - `remove` to remove a linked working tree and its administrative files (and - warn if the working tree is dirty) - - `mv` to move or rename a working tree and update its administrative files - GIT --- Part of the linkgit:git[1] suite diff --combined builtin/worktree.c index 4e7c98758f,f77ef994c4..1c1e576fdb --- a/builtin/worktree.c +++ b/builtin/worktree.c @@@ -14,10 -14,12 +14,12 @@@ #include "worktree.h" static const char * const worktree_usage[] = { - N_("git worktree add [] []"), + N_("git worktree add [] []"), N_("git worktree list []"), N_("git worktree lock [] "), + N_("git worktree move "), N_("git worktree prune []"), + N_("git worktree remove [] "), N_("git worktree unlock "), NULL }; @@@ -345,23 -347,9 +347,23 @@@ done * Hook failure does not warrant worktree deletion, so run hook after * is_junk is cleared, but do return appropriate code when hook fails. */ - if (!ret && opts->checkout) - ret = run_hook_le(NULL, "post-checkout", oid_to_hex(&null_oid), - oid_to_hex(&commit->object.oid), "1", NULL); + if (!ret && opts->checkout) { + const char *hook = find_hook("post-checkout"); + if (hook) { + const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL }; + cp.git_cmd = 0; + cp.no_stdin = 1; + cp.stdout_to_stderr = 1; + cp.dir = path; + cp.env = env; + cp.argv = NULL; + argv_array_pushl(&cp.args, absolute_path(hook), + oid_to_hex(&null_oid), + oid_to_hex(&commit->object.oid), + "1", NULL); + ret = run_command(&cp); + } + } argv_array_clear(&child_env); strbuf_release(&sb); @@@ -619,6 -607,219 +621,220 @@@ static int unlock_worktree(int ac, cons return ret; } + static void validate_no_submodules(const struct worktree *wt) + { + struct index_state istate = { NULL }; + int i, found_submodules = 0; + - if (read_index_from(&istate, worktree_git_path(wt, "index")) > 0) { ++ if (read_index_from(&istate, worktree_git_path(wt, "index"), ++ get_worktree_git_dir(wt)) > 0) { + for (i = 0; i < istate.cache_nr; i++) { + struct cache_entry *ce = istate.cache[i]; + + if (S_ISGITLINK(ce->ce_mode)) { + found_submodules = 1; + break; + } + } + } + discard_index(&istate); + + if (found_submodules) + die(_("working trees containing submodules cannot be moved or removed")); + } + + static int move_worktree(int ac, const char **av, const char *prefix) + { + struct option options[] = { + OPT_END() + }; + struct worktree **worktrees, *wt; + struct strbuf dst = STRBUF_INIT; + struct strbuf errmsg = STRBUF_INIT; + const char *reason; + char *path; + + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + if (ac != 2) + usage_with_options(worktree_usage, options); + + path = prefix_filename(prefix, av[1]); + strbuf_addstr(&dst, path); + free(path); + + worktrees = get_worktrees(0); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("'%s' is a main working tree"), av[0]); + if (is_directory(dst.buf)) { + const char *sep = find_last_dir_sep(wt->path); + + if (!sep) + die(_("could not figure out destination name from '%s'"), + wt->path); + strbuf_trim_trailing_dir_sep(&dst); + strbuf_addstr(&dst, sep); + } + if (file_exists(dst.buf)) + die(_("target '%s' already exists"), dst.buf); + + validate_no_submodules(wt); + + reason = is_worktree_locked(wt); + if (reason) { + if (*reason) + die(_("cannot move a locked working tree, lock reason: %s"), + reason); + die(_("cannot move a locked working tree")); + } + if (validate_worktree(wt, &errmsg, 0)) + die(_("validation failed, cannot move working tree: %s"), + errmsg.buf); + strbuf_release(&errmsg); + + if (rename(wt->path, dst.buf) == -1) + die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf); + + update_worktree_location(wt, dst.buf); + + strbuf_release(&dst); + free_worktrees(worktrees); + return 0; + } + + /* + * Note, "git status --porcelain" is used to determine if it's safe to + * delete a whole worktree. "git status" does not ignore user + * configuration, so if a normal "git status" shows "clean" for the + * user, then it's ok to remove it. + * + * This assumption may be a bad one. We may want to ignore + * (potentially bad) user settings and only delete a worktree when + * it's absolutely safe to do so from _our_ point of view because we + * know better. + */ + static void check_clean_worktree(struct worktree *wt, + const char *original_path) + { + struct argv_array child_env = ARGV_ARRAY_INIT; + struct child_process cp; + char buf[1]; + int ret; + + /* + * Until we sort this out, all submodules are "dirty" and + * will abort this function. + */ + validate_no_submodules(wt); + + argv_array_pushf(&child_env, "%s=%s/.git", + GIT_DIR_ENVIRONMENT, wt->path); + argv_array_pushf(&child_env, "%s=%s", + GIT_WORK_TREE_ENVIRONMENT, wt->path); + memset(&cp, 0, sizeof(cp)); + argv_array_pushl(&cp.args, "status", + "--porcelain", "--ignore-submodules=none", + NULL); + cp.env = child_env.argv; + cp.git_cmd = 1; + cp.dir = wt->path; + cp.out = -1; + ret = start_command(&cp); + if (ret) + die_errno(_("failed to run 'git status' on '%s'"), + original_path); + ret = xread(cp.out, buf, sizeof(buf)); + if (ret) + die(_("'%s' is dirty, use --force to delete it"), + original_path); + close(cp.out); + ret = finish_command(&cp); + if (ret) + die_errno(_("failed to run 'git status' on '%s', code %d"), + original_path, ret); + } + + static int delete_git_work_tree(struct worktree *wt) + { + struct strbuf sb = STRBUF_INIT; + int ret = 0; + + strbuf_addstr(&sb, wt->path); + if (remove_dir_recursively(&sb, 0)) { + error_errno(_("failed to delete '%s'"), sb.buf); + ret = -1; + } + strbuf_release(&sb); + 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) + { + int force = 0; + struct option options[] = { + OPT_BOOL(0, "force", &force, + N_("force removing even if the worktree is dirty")), + OPT_END() + }; + struct worktree **worktrees, *wt; + struct strbuf errmsg = STRBUF_INIT; + const char *reason; + int ret = 0; + + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + if (ac != 1) + usage_with_options(worktree_usage, options); + + worktrees = get_worktrees(0); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("'%s' is a main working tree"), av[0]); + reason = is_worktree_locked(wt); + if (reason) { + if (*reason) + die(_("cannot remove a locked working tree, lock reason: %s"), + reason); + die(_("cannot remove a locked working tree")); + } + if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK)) + die(_("validation failed, cannot remove working tree: %s"), + errmsg.buf); + strbuf_release(&errmsg); + + if (file_exists(wt->path)) { + if (!force) + check_clean_worktree(wt, av[0]); + + ret |= delete_git_work_tree(wt); + } + /* + * continue on even if ret is non-zero, there's no going back + * from here. + */ + ret |= delete_git_dir(wt); + + free_worktrees(worktrees); + return ret; + } + int cmd_worktree(int ac, const char **av, const char *prefix) { struct option options[] = { @@@ -641,5 -842,9 +857,9 @@@ return lock_worktree(ac - 1, av + 1, prefix); if (!strcmp(av[1], "unlock")) return unlock_worktree(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "move")) + return move_worktree(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "remove")) + return remove_worktree(ac - 1, av + 1, prefix); usage_with_options(worktree_usage, options); } diff --combined contrib/completion/git-completion.bash index 91536d831c,ff4a39631e..66010707df --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@@ -439,7 -439,7 +439,7 @@@ __git_refs ( track="" ;; *) - for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do + for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD REBASE_HEAD; do case "$i" in $match*) if [ -e "$dir/$i" ]; then @@@ -594,7 -594,7 +594,7 @@@ __git_is_configured_remote ( __git_list_merge_strategies () { - git merge -s help 2>&1 | + LANG=C LC_ALL=C git merge -s help 2>&1 | sed -n -e '/[Aa]vailable strategies are: /,/^$/{ s/\.$// s/.*:// @@@ -1077,7 -1077,7 +1077,7 @@@ _git_am ( { __git_find_repo_path if [ -d "$__git_repo_path"/rebase-apply ]; then - __gitcomp "--skip --continue --resolved --abort" + __gitcomp "--skip --continue --resolved --abort --quit --show-current-patch" return fi case "$cur" in @@@ -1468,7 -1468,7 +1468,7 @@@ __git_fetch_recurse_submodules="yes on- __git_fetch_options=" --quiet --verbose --append --upload-pack --force --keep --depth= --tags --no-tags --all --prune --dry-run --recurse-submodules= - --unshallow --update-shallow + --unshallow --update-shallow --prune-tags " _git_fetch () @@@ -1992,11 -1992,11 +1992,11 @@@ _git_rebase ( { __git_find_repo_path if [ -f "$__git_repo_path"/rebase-merge/interactive ]; then - __gitcomp "--continue --skip --abort --quit --edit-todo" + __gitcomp "--continue --skip --abort --quit --edit-todo --show-current-patch" return elif [ -d "$__git_repo_path"/rebase-apply ] || \ [ -d "$__git_repo_path"/rebase-merge ]; then - __gitcomp "--continue --skip --abort --quit" + __gitcomp "--continue --skip --abort --quit --show-current-patch" return fi __git_complete_strategy && return @@@ -3087,7 -3087,7 +3087,7 @@@ _git_whatchanged ( _git_worktree () { - local subcommands="add list lock prune unlock" + local subcommands="add list lock move prune remove unlock" local subcommand="$(__git_find_on_cmdline "$subcommands")" if [ -z "$subcommand" ]; then __gitcomp "$subcommands" @@@ -3105,6 -3105,9 +3105,9 @@@ prune,--*) __gitcomp "--dry-run --expire --verbose" ;; + remove,--*) + __gitcomp "--force" + ;; *) ;; esac diff --combined strbuf.c index 5f138ed3c8,46930ad027..0759590b3e --- a/strbuf.c +++ b/strbuf.c @@@ -95,6 -95,7 +95,7 @@@ void strbuf_trim(struct strbuf *sb strbuf_rtrim(sb); strbuf_ltrim(sb); } + void strbuf_rtrim(struct strbuf *sb) { while (sb->len > 0 && isspace((unsigned char)sb->buf[sb->len - 1])) @@@ -102,6 -103,13 +103,13 @@@ sb->buf[sb->len] = '\0'; } + void strbuf_trim_trailing_dir_sep(struct strbuf *sb) + { + while (sb->len > 0 && is_dir_sep((unsigned char)sb->buf[sb->len - 1])) + sb->len--; + sb->buf[sb->len] = '\0'; + } + void strbuf_ltrim(struct strbuf *sb) { char *b = sb->buf; @@@ -612,18 -620,14 +620,18 @@@ ssize_t strbuf_read_file(struct strbuf { int fd; ssize_t len; + int saved_errno; fd = open(path, O_RDONLY); if (fd < 0) return -1; len = strbuf_read(sb, fd, hint); + saved_errno = errno; close(fd); - if (len < 0) + if (len < 0) { + errno = saved_errno; return -1; + } return len; }