diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Documentation/git-prune.txt | 3 | ||||
-rw-r--r-- | Documentation/git-worktree.txt | 48 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | builtin.h | 1 | ||||
-rw-r--r-- | builtin/checkout.c | 23 | ||||
-rw-r--r-- | builtin/gc.c | 2 | ||||
-rw-r--r-- | builtin/prune.c | 99 | ||||
-rw-r--r-- | builtin/worktree.c | 133 | ||||
-rw-r--r-- | command-list.txt | 1 | ||||
-rw-r--r-- | git.c | 1 | ||||
-rwxr-xr-x | t/t2025-checkout-to.sh | 8 | ||||
-rwxr-xr-x | t/t2026-prune-linked-checkouts.sh | 22 |
13 files changed, 217 insertions, 126 deletions
diff --git a/.gitignore b/.gitignore index 422c5382c1..a685ec1fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,7 @@ /git-verify-tag /git-web--browse /git-whatchanged +/git-worktree /git-write-tree /git-core-*/?* /gitweb/GITWEB-BUILD-OPTIONS diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt index 1cf3bed4ab..7a493c80f7 100644 --- a/Documentation/git-prune.txt +++ b/Documentation/git-prune.txt @@ -48,9 +48,6 @@ OPTIONS --expire <time>:: Only expire loose objects older than <time>. ---worktrees:: - Prune dead working tree information in $GIT_DIR/worktrees. - <head>...:: In addition to objects reachable from any of our references, keep objects diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt new file mode 100644 index 0000000000..41103e5485 --- /dev/null +++ b/Documentation/git-worktree.txt @@ -0,0 +1,48 @@ +git-worktree(1) +=============== + +NAME +---- +git-worktree - Manage multiple worktrees + + +SYNOPSIS +-------- +[verse] +'git worktree prune' [-n] [-v] [--expire <expire>] + +DESCRIPTION +----------- + +Manage multiple worktrees attached to the same repository. These are +created by the command `git checkout --to`. + +COMMANDS +-------- +prune:: + +Prune working tree information in $GIT_DIR/worktrees. + +OPTIONS +------- + +-n:: +--dry-run:: + Do not remove anything; just report what it would + remove. + +-v:: +--verbose:: + Report all removals. + +--expire <time>:: + Only expire unused worktrees older than <time>. + +SEE ALSO +-------- + +linkgit:git-checkout[1] + +GIT +--- +Part of the linkgit:git[1] suite @@ -910,6 +910,7 @@ BUILTIN_OBJS += builtin/var.o BUILTIN_OBJS += builtin/verify-commit.o BUILTIN_OBJS += builtin/verify-pack.o BUILTIN_OBJS += builtin/verify-tag.o +BUILTIN_OBJS += builtin/worktree.o BUILTIN_OBJS += builtin/write-tree.o GITLIBS = $(LIB_FILE) $(XDIFF_LIB) @@ -133,6 +133,7 @@ extern int cmd_verify_commit(int argc, const char **argv, const char *prefix); extern int cmd_verify_tag(int argc, const char **argv, const char *prefix); extern int cmd_version(int argc, const char **argv, const char *prefix); extern int cmd_whatchanged(int argc, const char **argv, const char *prefix); +extern int cmd_worktree(int argc, const char **argv, const char *prefix); extern int cmd_write_tree(int argc, const char **argv, const char *prefix); extern int cmd_verify_pack(int argc, const char **argv, const char *prefix); extern int cmd_show_ref(int argc, const char **argv, const char *prefix); diff --git a/builtin/checkout.c b/builtin/checkout.c index 9b49f0e413..e227f64995 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1110,7 +1110,6 @@ static int parse_branchname_arg(int argc, const char **argv, { struct tree **source_tree = &opts->source_tree; const char **new_branch = &opts->new_branch; - int force_detach = opts->force_detach; int argcount = 0; unsigned char branch_rev[20]; const char *arg; @@ -1231,17 +1230,6 @@ static int parse_branchname_arg(int argc, const char **argv, else new->path = NULL; /* not an existing branch */ - if (new->path && !force_detach && !*new_branch) { - unsigned char sha1[20]; - int flag; - char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag); - if (head_ref && - (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)) && - !opts->ignore_other_worktrees) - check_linked_checkouts(new); - free(head_ref); - } - new->commit = lookup_commit_reference_gently(rev, 1); if (!new->commit) { /* not a commit */ @@ -1321,6 +1309,17 @@ static int checkout_branch(struct checkout_opts *opts, die(_("Cannot switch branch to a non-commit '%s'"), new->name); + if (new->path && !opts->force_detach && !opts->new_branch) { + unsigned char sha1[20]; + int flag; + char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag); + if (head_ref && + (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)) && + !opts->ignore_other_worktrees) + check_linked_checkouts(new); + free(head_ref); + } + if (opts->new_worktree) return prepare_linked_checkout(opts, new); diff --git a/builtin/gc.c b/builtin/gc.c index 36fe33300f..4957c39032 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -293,7 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix) argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL); argv_array_pushl(&repack, "repack", "-d", "-l", NULL); argv_array_pushl(&prune, "prune", "--expire", NULL); - argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL); + argv_array_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); argv_array_pushl(&rerere, "rerere", "gc", NULL); gc_config(); diff --git a/builtin/prune.c b/builtin/prune.c index 0c73246c72..10b03d3e4c 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -6,7 +6,6 @@ #include "reachable.h" #include "parse-options.h" #include "progress.h" -#include "dir.h" static const char * const prune_usage[] = { N_("git prune [-n] [-v] [--expire <time>] [--] [<head>...]"), @@ -76,95 +75,6 @@ static int prune_subdir(int nr, const char *path, void *data) return 0; } -static int prune_worktree(const char *id, struct strbuf *reason) -{ - struct stat st; - char *path; - int fd, len; - - if (!is_directory(git_path("worktrees/%s", id))) { - strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id); - 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); - 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)); - return 1; - } - len = st.st_size; - path = xmalloc(len + 1); - read_in_full(fd, path, len); - close(fd); - while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) - len--; - if (!len) { - strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id); - free(path); - return 1; - } - path[len] = '\0'; - if (!file_exists(path)) { - struct stat st_link; - free(path); - /* - * the repo is moved manually and has not been - * accessed since? - */ - if (!stat(git_path("worktrees/%s/link", id), &st_link) && - st_link.st_nlink > 1) - return 0; - if (st.st_mtime <= expire) { - strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id); - return 1; - } else { - return 0; - } - } - free(path); - return 0; -} - -static void prune_worktrees(void) -{ - struct strbuf reason = STRBUF_INIT; - struct strbuf path = STRBUF_INIT; - DIR *dir = opendir(git_path("worktrees")); - struct dirent *d; - int ret; - if (!dir) - return; - while ((d = readdir(dir)) != NULL) { - if (!strcmp(d->d_name, ".") || !strcmp(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; - strbuf_reset(&path); - strbuf_addstr(&path, git_path("worktrees/%s", d->d_name)); - ret = remove_dir_recursively(&path, 0); - if (ret < 0 && errno == ENOTDIR) - ret = unlink(path.buf); - if (ret) - error(_("failed to remove: %s"), strerror(errno)); - } - closedir(dir); - if (!show_only) - rmdir(git_path("worktrees")); - strbuf_release(&reason); - strbuf_release(&path); -} - /* * Write errors (particularly out of space) can result in * failed temporary packs (and more rarely indexes and other @@ -191,12 +101,10 @@ int cmd_prune(int argc, const char **argv, const char *prefix) { struct rev_info revs; struct progress *progress = NULL; - int do_prune_worktrees = 0; const struct option options[] = { OPT__DRY_RUN(&show_only, N_("do not remove, show only")), OPT__VERBOSE(&verbose, N_("report pruned objects")), OPT_BOOL(0, "progress", &show_progress, N_("show progress")), - OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")), OPT_EXPIRY_DATE(0, "expire", &expire, N_("expire objects older than <time>")), OPT_END() @@ -211,13 +119,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, prune_usage, 0); - if (do_prune_worktrees) { - if (argc) - die(_("--worktrees does not take extra arguments")); - prune_worktrees(); - return 0; - } - while (argc--) { unsigned char sha1[20]; const char *name = *argv++; diff --git a/builtin/worktree.c b/builtin/worktree.c new file mode 100644 index 0000000000..2a729c661c --- /dev/null +++ b/builtin/worktree.c @@ -0,0 +1,133 @@ +#include "cache.h" +#include "builtin.h" +#include "dir.h" +#include "parse-options.h" + +static const char * const worktree_usage[] = { + N_("git worktree prune [<options>]"), + NULL +}; + +static int show_only; +static int verbose; +static unsigned long expire; + +static int prune_worktree(const char *id, struct strbuf *reason) +{ + struct stat st; + char *path; + int fd, len; + + if (!is_directory(git_path("worktrees/%s", id))) { + strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id); + 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); + 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)); + return 1; + } + len = st.st_size; + path = xmalloc(len + 1); + read_in_full(fd, path, len); + close(fd); + while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) + len--; + if (!len) { + strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id); + free(path); + return 1; + } + path[len] = '\0'; + if (!file_exists(path)) { + struct stat st_link; + free(path); + /* + * the repo is moved manually and has not been + * accessed since? + */ + if (!stat(git_path("worktrees/%s/link", id), &st_link) && + st_link.st_nlink > 1) + return 0; + if (st.st_mtime <= expire) { + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id); + return 1; + } else { + return 0; + } + } + free(path); + return 0; +} + +static void prune_worktrees(void) +{ + struct strbuf reason = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + DIR *dir = opendir(git_path("worktrees")); + struct dirent *d; + int ret; + if (!dir) + return; + while ((d = readdir(dir)) != NULL) { + if (!strcmp(d->d_name, ".") || !strcmp(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; + strbuf_reset(&path); + strbuf_addstr(&path, git_path("worktrees/%s", d->d_name)); + ret = remove_dir_recursively(&path, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(path.buf); + if (ret) + error(_("failed to remove: %s"), strerror(errno)); + } + closedir(dir); + if (!show_only) + rmdir(git_path("worktrees")); + strbuf_release(&reason); + strbuf_release(&path); +} + +static int prune(int ac, const char **av, const char *prefix) +{ + struct option options[] = { + OPT__DRY_RUN(&show_only, N_("do not remove, show only")), + OPT__VERBOSE(&verbose, N_("report pruned objects")), + OPT_EXPIRY_DATE(0, "expire", &expire, + N_("expire objects older than <time>")), + OPT_END() + }; + + expire = ULONG_MAX; + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + if (ac) + usage_with_options(worktree_usage, options); + prune_worktrees(); + return 0; +} + +int cmd_worktree(int ac, const char **av, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + + if (ac < 2) + usage_with_options(worktree_usage, options); + if (!strcmp(av[1], "prune")) + return prune(ac - 1, av + 1, prefix); + usage_with_options(worktree_usage, options); +} diff --git a/command-list.txt b/command-list.txt index b17c011bfd..2a94137bbb 100644 --- a/command-list.txt +++ b/command-list.txt @@ -148,4 +148,5 @@ git-verify-pack plumbinginterrogators git-verify-tag ancillaryinterrogators gitweb ancillaryinterrogators git-whatchanged ancillaryinterrogators +git-worktree mainporcelain git-write-tree plumbingmanipulators @@ -483,6 +483,7 @@ static struct cmd_struct commands[] = { { "verify-tag", cmd_verify_tag, RUN_SETUP }, { "version", cmd_version }, { "whatchanged", cmd_whatchanged, RUN_SETUP }, + { "worktree", cmd_worktree, RUN_SETUP }, { "write-tree", cmd_write_tree, RUN_SETUP }, }; diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh index f8e4df4818..a8d93366f6 100755 --- a/t/t2025-checkout-to.sh +++ b/t/t2025-checkout-to.sh @@ -28,6 +28,14 @@ test_expect_success 'checkout --to refuses to checkout locked branch' ' ! test -d .git/worktrees/zere ' +test_expect_success 'checking out paths not complaining about linked checkouts' ' + ( + cd existing_empty && + echo dirty >>init.t && + git checkout master -- init.t + ) +' + test_expect_success 'checkout --to a new worktree' ' git rev-parse HEAD >expect && git checkout --detach --to here master && diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh index 1821a480c5..e872f02dac 100755 --- a/t/t2026-prune-linked-checkouts.sh +++ b/t/t2026-prune-linked-checkouts.sh @@ -8,15 +8,15 @@ test_expect_success initialize ' git commit --allow-empty -m init ' -test_expect_success 'prune --worktrees on normal repo' ' - git prune --worktrees && - test_must_fail git prune --worktrees abc +test_expect_success 'worktree prune on normal repo' ' + git worktree prune && + test_must_fail git worktree prune abc ' test_expect_success 'prune files inside $GIT_DIR/worktrees' ' mkdir .git/worktrees && : >.git/worktrees/abc && - git prune --worktrees --verbose >actual && + git worktree prune --verbose >actual && cat >expect <<EOF && Removing worktrees/abc: not a valid directory EOF @@ -31,7 +31,7 @@ test_expect_success 'prune directories without gitdir' ' cat >expect <<EOF && Removing worktrees/def: gitdir file does not exist EOF - git prune --worktrees --verbose >actual && + git worktree prune --verbose >actual && test_i18ncmp expect actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -42,7 +42,7 @@ test_expect_success SANITY 'prune directories with unreadable gitdir' ' : >.git/worktrees/def/def && : >.git/worktrees/def/gitdir && chmod u-r .git/worktrees/def/gitdir && - git prune --worktrees --verbose >actual && + git worktree prune --verbose >actual && test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -52,7 +52,7 @@ test_expect_success 'prune directories with invalid gitdir' ' mkdir -p .git/worktrees/def/abc && : >.git/worktrees/def/def && : >.git/worktrees/def/gitdir && - git prune --worktrees --verbose >actual && + git worktree prune --verbose >actual && test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -62,7 +62,7 @@ test_expect_success 'prune directories with gitdir pointing to nowhere' ' mkdir -p .git/worktrees/def/abc && : >.git/worktrees/def/def && echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir && - git prune --worktrees --verbose >actual && + git worktree prune --verbose >actual && test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -72,7 +72,7 @@ test_expect_success 'not prune locked checkout' ' test_when_finished rm -r .git/worktrees && mkdir -p .git/worktrees/ghi && : >.git/worktrees/ghi/locked && - git prune --worktrees && + git worktree prune && test -d .git/worktrees/ghi ' @@ -82,14 +82,14 @@ test_expect_success 'not prune recent checkouts' ' mkdir -p .git/worktrees/jlm && echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir && rmdir zz && - git prune --worktrees --verbose --expire=2.days.ago && + git worktree prune --verbose --expire=2.days.ago && test -d .git/worktrees/jlm ' test_expect_success 'not prune proper checkouts' ' test_when_finished rm -r .git/worktrees && git checkout "--to=$PWD/nop" --detach master && - git prune --worktrees && + git worktree prune && test -d .git/worktrees/nop ' |