diff options
Diffstat (limited to 'builtin/worktree.c')
-rw-r--r-- | builtin/worktree.c | 276 |
1 files changed, 189 insertions, 87 deletions
diff --git a/builtin/worktree.c b/builtin/worktree.c index d6bc5263f1..99abaeec6c 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -4,7 +4,7 @@ #include "builtin.h" #include "dir.h" #include "parse-options.h" -#include "argv-array.h" +#include "strvec.h" #include "branch.h" #include "refs.h" #include "run-command.h" @@ -67,7 +67,12 @@ 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) +/* + * Return true if worktree entry should be pruned, along with the reason for + * pruning. Otherwise, return false and the worktree's path, or NULL if it + * cannot be determined. Caller is responsible for freeing returned path. + */ +static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath) { struct stat st; char *path; @@ -75,20 +80,21 @@ static int prune_worktree(const char *id, struct strbuf *reason) size_t len; ssize_t read_result; + *wtpath = NULL; if (!is_directory(git_path("worktrees/%s", id))) { - strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id); + strbuf_addstr(reason, _("not a valid directory")); return 1; } if (file_exists(git_path("worktrees/%s/locked", id))) return 0; if (stat(git_path("worktrees/%s/gitdir", id), &st)) { - strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id); + strbuf_addstr(reason, _("gitdir file does not exist")); return 1; } fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY); if (fd < 0) { - strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"), - id, strerror(errno)); + strbuf_addf(reason, _("unable to read gitdir file (%s)"), + strerror(errno)); return 1; } len = xsize_t(st.st_size); @@ -96,8 +102,8 @@ static int prune_worktree(const char *id, struct strbuf *reason) read_result = read_in_full(fd, path, len); if (read_result < 0) { - strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"), - id, strerror(errno)); + strbuf_addf(reason, _("unable to read gitdir file (%s)"), + strerror(errno)); close(fd); free(path); return 1; @@ -106,53 +112,103 @@ static int prune_worktree(const char *id, struct strbuf *reason) if (read_result != len) { strbuf_addf(reason, - _("Removing worktrees/%s: short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"), - id, (uintmax_t)len, (uintmax_t)read_result); + _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"), + (uintmax_t)len, (uintmax_t)read_result); free(path); return 1; } while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) len--; if (!len) { - strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id); + strbuf_addstr(reason, _("invalid gitdir file")); free(path); return 1; } path[len] = '\0'; if (!file_exists(path)) { - free(path); if (stat(git_path("worktrees/%s/index", id), &st) || st.st_mtime <= expire) { - strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id); + strbuf_addstr(reason, _("gitdir file points to non-existent location")); + free(path); return 1; } else { + *wtpath = path; return 0; } } - free(path); + *wtpath = path; return 0; } +static void prune_worktree(const char *id, const char *reason) +{ + if (show_only || verbose) + printf_ln(_("Removing %s/%s: %s"), "worktrees", id, reason); + if (!show_only) + delete_git_dir(id); +} + +static int prune_cmp(const void *a, const void *b) +{ + const struct string_list_item *x = a; + const struct string_list_item *y = b; + int c; + + if ((c = fspathcmp(x->string, y->string))) + return c; + /* + * paths same; prune_dupes() removes all but the first worktree entry + * having the same path, so sort main worktree ('util' is NULL) above + * linked worktrees ('util' not NULL) since main worktree can't be + * removed + */ + if (!x->util) + return -1; + if (!y->util) + return 1; + /* paths same; sort by .git/worktrees/<id> */ + return strcmp(x->util, y->util); +} + +static void prune_dups(struct string_list *l) +{ + int i; + + QSORT(l->items, l->nr, prune_cmp); + for (i = 1; i < l->nr; i++) { + if (!fspathcmp(l->items[i].string, l->items[i - 1].string)) + prune_worktree(l->items[i].util, "duplicate entry"); + } +} + static void prune_worktrees(void) { struct strbuf reason = STRBUF_INIT; + struct strbuf main_path = STRBUF_INIT; + struct string_list kept = STRING_LIST_INIT_NODUP; DIR *dir = opendir(git_path("worktrees")); struct dirent *d; if (!dir) return; while ((d = readdir(dir)) != NULL) { + char *path; if (is_dot_or_dotdot(d->d_name)) continue; strbuf_reset(&reason); - if (!prune_worktree(d->d_name, &reason)) - continue; - if (show_only || verbose) - printf("%s\n", reason.buf); - if (show_only) - continue; - delete_git_dir(d->d_name); + if (should_prune_worktree(d->d_name, &reason, &path)) + prune_worktree(d->d_name, reason.buf); + else if (path) + string_list_append(&kept, path)->util = xstrdup(d->d_name); } closedir(dir); + + strbuf_add_absolute_path(&main_path, get_git_common_dir()); + /* massage main worktree absolute path to match 'gitdir' content */ + strbuf_strip_suffix(&main_path, "/."); + string_list_append(&kept, strbuf_detach(&main_path, NULL)); + prune_dups(&kept); + string_list_clear(&kept, 1); + if (!show_only) delete_worktrees_dir_if_empty(); strbuf_release(&reason); @@ -224,59 +280,55 @@ static const char *worktree_basename(const char *path, int *olen) return name; } -static void validate_worktree_add(const char *path, const struct add_opts *opts) +/* check that path is viable location for worktree */ +static void check_candidate_path(const char *path, + int force, + struct worktree **worktrees, + const char *cmd) { - 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); + wt = find_worktree_by_path(worktrees, path); if (!wt) - goto done; + return; locked = !!worktree_lock_reason(wt); - if ((!locked && opts->force) || (locked && opts->force > 1)) { + if ((!locked && force) || (locked && force > 1)) { if (delete_git_dir(wt->id)) - die(_("unable to re-add worktree '%s'"), path); - goto done; + die(_("unusable worktree destination '%s'"), path); + return; } 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); + die(_("'%s' is a missing but locked worktree;\nuse '%s -f -f' to override, or 'unlock' and 'prune' or 'remove' to clear"), cmd, 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); + die(_("'%s' is a missing but already registered worktree;\nuse '%s -f' to override, or 'prune' or 'remove' to clear"), cmd, path); } static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; - struct strbuf sb = STRBUF_INIT; + struct strbuf sb = STRBUF_INIT, realpath = STRBUF_INIT; const char *name; struct child_process cp = CHILD_PROCESS_INIT; - struct argv_array child_env = ARGV_ARRAY_INIT; + struct strvec child_env = STRVEC_INIT; unsigned int counter = 0; int len, ret; struct strbuf symref = STRBUF_INIT; struct commit *commit = NULL; int is_branch = 0; struct strbuf sb_name = STRBUF_INIT; + struct worktree **worktrees; - validate_worktree_add(path, opts); + worktrees = get_worktrees(); + check_candidate_path(path, opts->force, worktrees, "add"); + free_worktrees(worktrees); + worktrees = NULL; /* is 'refname' a branch or commit? */ if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && @@ -337,9 +389,11 @@ static int add_worktree(const char *path, const char *refname, strbuf_reset(&sb); strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); - write_file(sb.buf, "%s", real_path(sb_git.buf)); + strbuf_realpath(&realpath, sb_git.buf, 1); + write_file(sb.buf, "%s", realpath.buf); + strbuf_realpath(&realpath, get_git_common_dir(), 1); write_file(sb_git.buf, "gitdir: %s/worktrees/%s", - real_path(get_git_common_dir()), name); + realpath.buf, name); /* * This is to keep resolve_ref() happy. We need a valid HEAD * or is_git_directory() will reject the directory. Any value which @@ -354,32 +408,32 @@ static int add_worktree(const char *path, const char *refname, strbuf_addf(&sb, "%s/commondir", sb_repo.buf); write_file(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); + strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf); + strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); cp.git_cmd = 1; if (!is_branch) - argv_array_pushl(&cp.args, "update-ref", "HEAD", - oid_to_hex(&commit->object.oid), NULL); + strvec_pushl(&cp.args, "update-ref", "HEAD", + oid_to_hex(&commit->object.oid), NULL); else { - argv_array_pushl(&cp.args, "symbolic-ref", "HEAD", - symref.buf, NULL); + strvec_pushl(&cp.args, "symbolic-ref", "HEAD", + symref.buf, NULL); if (opts->quiet) - argv_array_push(&cp.args, "--quiet"); + strvec_push(&cp.args, "--quiet"); } - cp.env = child_env.argv; + cp.env = child_env.v; ret = run_command(&cp); if (ret) goto done; if (opts->checkout) { cp.argv = NULL; - argv_array_clear(&cp.args); - argv_array_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); + strvec_clear(&cp.args); + strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) - argv_array_push(&cp.args, "--quiet"); - cp.env = child_env.argv; + strvec_push(&cp.args, "--quiet"); + cp.env = child_env.v; ret = run_command(&cp); if (ret) goto done; @@ -411,20 +465,21 @@ done: cp.env = env; cp.argv = NULL; cp.trace2_hook_name = "post-checkout"; - argv_array_pushl(&cp.args, absolute_path(hook), - oid_to_hex(&null_oid), - oid_to_hex(&commit->object.oid), - "1", NULL); + strvec_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); + strvec_clear(&child_env); strbuf_release(&sb); strbuf_release(&symref); strbuf_release(&sb_repo); strbuf_release(&sb_git); strbuf_release(&sb_name); + strbuf_release(&realpath); return ret; } @@ -500,7 +555,7 @@ static int add(int ac, const char **av, const char *prefix) N_("create a new branch")), OPT_STRING('B', NULL, &new_branch_force, N_("branch"), N_("create or reset a branch")), - OPT_BOOL(0, "detach", &opts.detach, N_("detach HEAD at named commit")), + OPT_BOOL('d', "detach", &opts.detach, N_("detach HEAD at named commit")), OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")), OPT_BOOL(0, "lock", &opts.keep_locked, N_("keep the new working tree locked")), OPT__QUIET(&opts.quiet, N_("suppress progress reporting")), @@ -564,15 +619,15 @@ static int add(int ac, const char **av, const char *prefix) if (new_branch) { struct child_process cp = CHILD_PROCESS_INIT; cp.git_cmd = 1; - argv_array_push(&cp.args, "branch"); + strvec_push(&cp.args, "branch"); if (new_branch_force) - argv_array_push(&cp.args, "--force"); + strvec_push(&cp.args, "--force"); if (opts.quiet) - argv_array_push(&cp.args, "--quiet"); - argv_array_push(&cp.args, new_branch); - argv_array_push(&cp.args, branch); + strvec_push(&cp.args, "--quiet"); + strvec_push(&cp.args, new_branch); + strvec_push(&cp.args, branch); if (opt_track) - argv_array_push(&cp.args, opt_track); + strvec_push(&cp.args, opt_track); if (run_command(&cp)) return -1; branch = new_branch; @@ -642,6 +697,23 @@ static void measure_widths(struct worktree **wt, int *abbrev, int *maxlen) } } +static int pathcmp(const void *a_, const void *b_) +{ + const struct worktree *const *a = a_; + const struct worktree *const *b = b_; + return fspathcmp((*a)->path, (*b)->path); +} + +static void pathsort(struct worktree **wt) +{ + int n = 0; + struct worktree **p = wt; + + while (*p++) + n++; + QSORT(wt, n, pathcmp); +} + static int list(int ac, const char **av, const char *prefix) { int porcelain = 0; @@ -655,9 +727,12 @@ static int list(int ac, const char **av, const char *prefix) if (ac) usage_with_options(worktree_usage, options); else { - struct worktree **worktrees = get_worktrees(GWT_SORT_LINKED); + struct worktree **worktrees = get_worktrees(); int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i; + /* sort worktrees by path but keep main worktree at top */ + pathsort(worktrees + 1); + if (!porcelain) measure_widths(worktrees, &abbrev, &path_maxlen); @@ -686,7 +761,7 @@ static int lock_worktree(int ac, const char **av, const char *prefix) if (ac != 1) usage_with_options(worktree_usage, options); - worktrees = get_worktrees(0); + worktrees = get_worktrees(); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -719,7 +794,7 @@ static int unlock_worktree(int ac, const char **av, const char *prefix) if (ac != 1) usage_with_options(worktree_usage, options); - worktrees = get_worktrees(0); + worktrees = get_worktrees(); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -793,7 +868,7 @@ static int move_worktree(int ac, const char **av, const char *prefix) strbuf_addstr(&dst, path); free(path); - worktrees = get_worktrees(0); + worktrees = get_worktrees(); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -808,8 +883,7 @@ static int move_worktree(int ac, const char **av, const char *prefix) strbuf_trim_trailing_dir_sep(&dst); strbuf_addstr(&dst, sep); } - if (file_exists(dst.buf)) - die(_("target '%s' already exists"), dst.buf); + check_candidate_path(dst.buf, force, worktrees, "move"); validate_no_submodules(wt); @@ -850,7 +924,6 @@ static int move_worktree(int ac, const char **av, const char *prefix) 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; @@ -861,15 +934,14 @@ static void check_clean_worktree(struct worktree *wt, */ 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; + child_process_init(&cp); + strvec_pushf(&cp.env_array, "%s=%s/.git", + GIT_DIR_ENVIRONMENT, wt->path); + strvec_pushf(&cp.env_array, "%s=%s", + GIT_WORK_TREE_ENVIRONMENT, wt->path); + strvec_pushl(&cp.args, "status", + "--porcelain", "--ignore-submodules=none", + NULL); cp.git_cmd = 1; cp.dir = wt->path; cp.out = -1; @@ -920,7 +992,7 @@ static int remove_worktree(int ac, const char **av, const char *prefix) if (ac != 1) usage_with_options(worktree_usage, options); - worktrees = get_worktrees(0); + worktrees = get_worktrees(); wt = find_worktree(worktrees, prefix, av[0]); if (!wt) die(_("'%s' is not a working tree"), av[0]); @@ -956,6 +1028,34 @@ static int remove_worktree(int ac, const char **av, const char *prefix) return ret; } +static void report_repair(int iserr, const char *path, const char *msg, void *cb_data) +{ + if (!iserr) { + printf_ln(_("repair: %s: %s"), msg, path); + } else { + int *exit_status = (int *)cb_data; + fprintf_ln(stderr, _("error: %s: %s"), msg, path); + *exit_status = 1; + } +} + +static int repair(int ac, const char **av, const char *prefix) +{ + const char **p; + const char *self[] = { ".", NULL }; + struct option options[] = { + OPT_END() + }; + int rc = 0; + + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + repair_worktrees(report_repair, &rc); + p = ac > 0 ? av : self; + for (; *p; p++) + repair_worktree_at_path(*p, report_repair, &rc); + return rc; +} + int cmd_worktree(int ac, const char **av, const char *prefix) { struct option options[] = { @@ -982,5 +1082,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix) return move_worktree(ac - 1, av + 1, prefix); if (!strcmp(av[1], "remove")) return remove_worktree(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "repair")) + return repair(ac - 1, av + 1, prefix); usage_with_options(worktree_usage, options); } |