Merge branch 'nd/multiple-work-trees'
authorJunio C Hamano <gitster@pobox.com>
Mon, 13 Jul 2015 21:02:02 +0000 (14:02 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 13 Jul 2015 21:02:02 +0000 (14:02 -0700)
"git checkout [<tree-ish>] <paths>" spent unnecessary cycles
checking if the current branch was checked out elsewhere, when we
know we are not switching the branches ourselves.

* nd/multiple-work-trees:
worktree: new place for "git prune --worktrees"
checkout: don't check worktrees when not necessary

13 files changed:
.gitignore
Documentation/git-prune.txt
Documentation/git-worktree.txt [new file with mode: 0644]
Makefile
builtin.h
builtin/checkout.c
builtin/gc.c
builtin/prune.c
builtin/worktree.c [new file with mode: 0644]
command-list.txt
git.c
t/t2025-checkout-to.sh
t/t2026-prune-linked-checkouts.sh
index 422c5382c1acfde24223fda6236dd3586456a263..a685ec1fb0ca49607431a65f1ccf035bb9b95a3a 100644 (file)
 /git-verify-tag
 /git-web--browse
 /git-whatchanged
+/git-worktree
 /git-write-tree
 /git-core-*/?*
 /gitweb/GITWEB-BUILD-OPTIONS
index 1cf3bed4ab7ab19364c6801fbc73920170dfd1e4..7a493c80f776092265abd4fb2575639e683bcf5c 100644 (file)
@@ -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 (file)
index 0000000..41103e5
--- /dev/null
@@ -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
index 895f002869045288dfc6298e596c020aa4bc8efd..8c3c724a7f5e0594ad836065757371f7a144c472 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -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)
index b87df70f96d0f43e4b838f34d7492d116d4d97db..9e04f979bc6ff251bcd18efd55db986d77b1c6aa 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -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);
index 9b49f0e413ba03ebfee924d514a175d8f74fe1d3..e227f649955a21647381ee322af65322b075b80d 100644 (file)
@@ -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);
 
index 36fe33300f644fc9c7e5139b452a9703548ba2a2..4957c3903293c2d4262f49c01188973bd3e53aa2 100644 (file)
@@ -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();
index 0c73246c721b3307f0d68b4e58f7c684aebce2e9..10b03d3e4cb5ced78118251ac390dd6a33f9a71b 100644 (file)
@@ -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 (file)
index 0000000..2a729c6
--- /dev/null
@@ -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);
+}
index b17c011bfd3dc7693a00ee6faf20a7a0608c7b7e..2a94137bbb383fda655b481446d0891062d73cb9 100644 (file)
@@ -148,4 +148,5 @@ git-verify-pack                         plumbinginterrogators
 git-verify-tag                          ancillaryinterrogators
 gitweb                                  ancillaryinterrogators
 git-whatchanged                         ancillaryinterrogators
+git-worktree                            mainporcelain
 git-write-tree                          plumbingmanipulators
diff --git a/git.c b/git.c
index 44374b1d9b6b73b3669f0f851bee058f45ee4f32..fa77bc92bf75e5edda1a1f68be63e8591145b11b 100644 (file)
--- a/git.c
+++ b/git.c
@@ -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 },
 };
 
index f8e4df4818523de74ff7330f015415bfb9cc8227..a8d93366f6bff23ec8edab5227f4931215a6dd3d 100755 (executable)
@@ -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 &&
index 1821a480c5ce6fd31416201e3a3dcb0f17b1de91..e872f02dac37638a64918d1e3f585aacd2f94516 100755 (executable)
@@ -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
 '