Signed-off-by: Nguyễn Thái Ngọc Duy <pclo...@gmail.com>
---
 Documentation/git-checkout.txt         |  6 +++
 Documentation/gitrepository-layout.txt |  3 +-
 builtin/checkout.c                     | 94 ++++++++++++++++++++++++++++++++++
 path.c                                 |  3 +-
 4 files changed, 104 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 91294f8..06076c8 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,12 @@ 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 repository is initialized at `<path>` that
+       shares everything with the current repository except working
+       directory specific files.
+
 <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
diff --git a/Documentation/gitrepository-layout.txt 
b/Documentation/gitrepository-layout.txt
index 7ce31d4..3c6149e 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -222,7 +222,8 @@ repos/<id>::
        If a repository's .git is a file contains two lines `gitsuper:
        <path>` and `repo: <id>`. The directory `<path>/repos/<id>`
        contains the real non-shared part of .git directory of the
-       repository in question (e.g. HEAD or index).
+       repository in question (e.g. HEAD or index). Such .git files
+       are created by `git checkout --to`.
 
 SEE ALSO
 --------
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 904fd71..95a1a61 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,9 @@ struct checkout_opts {
        const char *prefix;
        struct pathspec pathspec;
        struct tree *source_tree;
+
+       const char *new_worktree;
+       const char **saved_argv;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -250,6 +253,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);
@@ -808,6 +814,82 @@ static int switch_branches(const struct checkout_opts 
*opts,
        return ret || writeout_error;
 }
 
+static void write_to_file(const char *path, const char *fmt, ...)
+{
+       va_list params;
+       FILE *fp = fopen(path, "w");
+       if (!fp)
+               die_errno(_("could not create file '%s'"), path);
+       va_start(params, fmt);
+       vfprintf(fp, fmt, params);
+       va_end(params);
+       fclose(fp);
+}
+
+static int checkout_new_worktree(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_to_file(sb_git.buf, "gitsuper: %s\ngitdir: %s\n",
+                     real_path(get_git_dir()), name);
+       strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+       write_to_file(sb.buf, "%s\n", sha1_to_hex(new->commit->object.sha1));
+
+       if (!opts->quiet)
+               fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+       /*
+        * Rerun checkout in the new worktree. This way is safer than
+        * set_git_dir() because a path relative to cwd could have
+        * been cached somewhere undetected.
+        */
+       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")) {
@@ -1069,6 +1151,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 checkout_new_worktree(opts, new);
+
        if (!new->commit && opts->new_branch) {
                unsigned char rev[20];
                int flag;
@@ -1111,6 +1196,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(),
        };
 
@@ -1119,6 +1206,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);
 
@@ -1127,6 +1217,10 @@ int cmd_checkout(int argc, const char **argv, const char 
*prefix)
        argc = parse_options(argc, argv, prefix, options, checkout_usage,
                             PARSE_OPT_KEEP_DASHDASH);
 
+       if (getenv("GIT_CHECKOUT_NEW_WORKTREE"))
+               /* recursive execution from checkout_new_worktree() */
+               opts.new_worktree = NULL;
+
        if (conflict_style) {
                opts.merge = 1; /* implied */
                git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/path.c b/path.c
index e51fc35..1a2478a 100644
--- a/path.c
+++ b/path.c
@@ -93,7 +93,8 @@ static void update_super_dir(char *buf, size_t n, int 
git_dir_len)
 {
        const char *super_dir_list[] = {
                "branches", "hooks", "info", "logs", "modules",
-               "objects", "refs", "remotes", "rr-cache", "svn", NULL
+               "objects", "refs", "remotes", "repos", "rr-cache",
+               "svn", NULL
        };
        const char *super_top_file_list[] = {
                "config", "packed-refs", "shallow", NULL
-- 
1.8.5.1.77.g42c48fa

--
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