"git checkout --to" sets up a new working directory with a .git file
pointing to $GIT_DIR/repos/<id>. It then executes "git checkout" again
on the new worktree with the same arguments except "--to" is taken
out. The second checkout execution, which is not contaminated with any
info from the current repository, will actually check out and
everything that normal "git checkout" does.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclo...@gmail.com>
---
 Documentation/git-checkout.txt         | 34 +++++++++++++
 Documentation/gitrepository-layout.txt |  3 ++
 builtin/checkout.c                     | 93 +++++++++++++++++++++++++++++++++-
 t/t2025-checkout-to.sh (new +x)        | 39 ++++++++++++++
 4 files changed, 167 insertions(+), 2 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 33ad2ad..fcf73b2 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to 
selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
+--to=<path>::
+       Check out a new branch in a separate working directory at
+       `<path>`. A new working directory is linked to the current
+       repository, sharing everything except working directory
+       specific files such as HEAD, index... See "MULTIPLE CHECKOUT
+       MODE" section for more information.
+
 <branch>::
        Branch to checkout; if it refers to a branch (i.e., a name that,
        when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +395,33 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
+MULTIPLE CHECKOUT MODE
+-------------------------------
+Normally a working directory is attached to repository. When "git
+checkout --to" is used, a new working directory is attached to the
+current repository. This new working directory is called "linked
+checkout" as compared to the "main checkout" prepared by "git init" or
+"git clone". A repository has one main checkout and zero or more
+linked checkouts.
+
+All checkouts share the same repository. Linked checkouts see the
+repository a bit different from the main checkout. When the checkout
+"new" reads the path $GIT_DIR/HEAD for example, the actual path
+returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts
+won't step on each other.
+
+Each linked checkout has a private space in $GIT_DIR/repos, usually
+named after the base name of the working directory with a number added
+to make it unique. The linked checkout's $GIT_DIR points to this
+private space while $GIT_COMMON_DIR points to the main checkout's
+$GIT_DIR. These settings are done by "git checkout --to".
+
+Because in this mode $GIT_DIR becomes a lightweight virtual file
+system where a path could be rewritten to some place else, accessing
+$GIT_DIR from scripts should use `git rev-parse --git-path` to resolve
+a path instead of using it directly unless the path is known to be
+private to the working directory.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/gitrepository-layout.txt 
b/Documentation/gitrepository-layout.txt
index 9bfe0f1..495a937 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -218,6 +218,9 @@ commondir::
 modules::
        Contains the git-repositories of the submodules.
 
+repos::
+       Contains worktree specific information of linked checkouts.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 0570e41..2b856a6 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,10 @@ struct checkout_opts {
        const char *prefix;
        struct pathspec pathspec;
        struct tree *source_tree;
+
+       const char *new_worktree;
+       const char **saved_argv;
+       int new_worktree_mode;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -250,6 +254,9 @@ static int checkout_paths(const struct checkout_opts *opts,
                die(_("Cannot update paths and switch to branch '%s' at the 
same time."),
                    opts->new_branch);
 
+       if (opts->new_worktree)
+               die(_("'%s' cannot be used with updating paths"), "--to");
+
        if (opts->patch_mode)
                return run_add_interactive(revision, "--patch=checkout",
                                           &opts->pathspec);
@@ -486,7 +493,7 @@ static int merge_working_tree(const struct checkout_opts 
*opts,
                        topts.dir->flags |= DIR_SHOW_IGNORED;
                        setup_standard_excludes(topts.dir);
                }
-               tree = parse_tree_indirect(old->commit ?
+               tree = parse_tree_indirect(old->commit && 
!opts->new_worktree_mode ?
                                           old->commit->object.sha1 :
                                           EMPTY_TREE_SHA1_BIN);
                init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -796,7 +803,8 @@ static int switch_branches(const struct checkout_opts *opts,
                return ret;
        }
 
-       if (!opts->quiet && !old.path && old.commit && new->commit != 
old.commit)
+       if (!opts->quiet && !old.path && old.commit &&
+           new->commit != old.commit && !opts->new_worktree_mode)
                orphaned_commit_warning(old.commit, new->commit);
 
        update_refs_for_switch(opts, &old, new);
@@ -806,6 +814,74 @@ static int switch_branches(const struct checkout_opts 
*opts,
        return ret || writeout_error;
 }
 
+static int prepare_linked_checkout(const struct checkout_opts *opts,
+                                  struct branch_info *new)
+{
+       struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
+       struct strbuf sb = STRBUF_INIT;
+       const char *path = opts->new_worktree;
+       struct stat st;
+       const char *name;
+       struct child_process cp;
+       int counter = 0, len;
+
+       if (!new->commit)
+               die(_("no branch specified"));
+
+       len = strlen(path);
+       if (!len || is_dir_sep(path[len - 1]))
+               die(_("'--to' argument '%s' cannot end with a slash"), path);
+
+       for (name = path + len - 1; name > path; name--)
+               if (is_dir_sep(*name)) {
+                       name++;
+                       break;
+               }
+       strbuf_addstr(&sb_repo, git_path("repos/%s", name));
+       len = sb_repo.len;
+       if (safe_create_leading_directories_const(sb_repo.buf))
+               die_errno(_("could not create leading directories of '%s'"),
+                         sb_repo.buf);
+       while (!stat(sb_repo.buf, &st)) {
+               counter++;
+               strbuf_setlen(&sb_repo, len);
+               strbuf_addf(&sb_repo, "%d", counter);
+       }
+       name = sb_repo.buf + len - strlen(name);
+       if (mkdir(sb_repo.buf, 0777))
+               die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+
+       strbuf_addf(&sb_git, "%s/.git", path);
+       if (safe_create_leading_directories_const(sb_git.buf))
+               die_errno(_("could not create leading directories of '%s'"),
+                         sb_git.buf);
+
+       write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
+                  real_path(get_git_dir()), name);
+       /*
+        * This is to keep resolve_ref() happy. We need a valid HEAD
+        * or is_git_directory() will reject the directory. Any valid
+        * value would do because this value will be ignored and
+        * replaced at the next (real) checkout.
+        */
+       strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+       write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
+       strbuf_reset(&sb);
+       strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
+       write_file(sb.buf, 1, "../..\n");
+
+       if (!opts->quiet)
+               fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+       setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
+       setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
+       setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
+       memset(&cp, 0, sizeof(cp));
+       cp.git_cmd = 1;
+       cp.argv = opts->saved_argv;
+       return run_command(&cp);
+}
+
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
        if (!strcmp(var, "diff.ignoresubmodules")) {
@@ -1067,6 +1143,9 @@ static int checkout_branch(struct checkout_opts *opts,
                die(_("Cannot switch branch to a non-commit '%s'"),
                    new->name);
 
+       if (opts->new_worktree)
+               return prepare_linked_checkout(opts, new);
+
        if (!new->commit && opts->new_branch) {
                unsigned char rev[20];
                int flag;
@@ -1109,6 +1188,8 @@ int cmd_checkout(int argc, const char **argv, const char 
*prefix)
                         N_("do not limit pathspecs to sparse entries only")),
                OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
                                N_("second guess 'git checkout 
no-such-branch'")),
+               OPT_STRING(0, "to", &opts.new_worktree, N_("path"),
+                          N_("check a branch out in a separate working 
directory")),
                OPT_END(),
        };
 
@@ -1117,6 +1198,9 @@ int cmd_checkout(int argc, const char **argv, const char 
*prefix)
        opts.overwrite_ignore = 1;
        opts.prefix = prefix;
 
+       opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
+       memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
+
        gitmodules_config();
        git_config(git_checkout_config, &opts);
 
@@ -1125,6 +1209,11 @@ int cmd_checkout(int argc, const char **argv, const char 
*prefix)
        argc = parse_options(argc, argv, prefix, options, checkout_usage,
                             PARSE_OPT_KEEP_DASHDASH);
 
+       /* recursive execution from checkout_new_worktree() */
+       opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
+       if (opts.new_worktree_mode)
+               opts.new_worktree = NULL;
+
        if (conflict_style) {
                opts.merge = 1; /* implied */
                git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 0000000..76eae4a
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       test_commit init
+'
+
+test_expect_success 'checkout --to not updating paths' '
+       test_must_fail git checkout --to -- init.t
+'
+
+test_expect_success 'checkout --to a new worktree' '
+       git checkout --to here master &&
+       (
+               cd here &&
+               test_cmp ../init.t init.t &&
+               git symbolic-ref HEAD >actual &&
+               echo refs/heads/master >expect &&
+               test_cmp expect actual &&
+               git fsck
+       )
+'
+
+test_expect_success 'checkout --to a new worktree creating new branch' '
+       git checkout --to there -b newmaster master &&
+       (
+               cd there &&
+               test_cmp ../init.t init.t &&
+               git symbolic-ref HEAD >actual &&
+               echo refs/heads/newmaster >expect &&
+               test_cmp expect actual &&
+               git fsck
+       )
+'
+
+test_done
-- 
1.8.5.2.240.g8478abd

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to