diff options
Diffstat (limited to 'builtin/checkout.c')
-rw-r--r-- | builtin/checkout.c | 286 |
1 files changed, 269 insertions, 17 deletions
diff --git a/builtin/checkout.c b/builtin/checkout.c index 52d6cbb0a8..2f92328db4 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -20,10 +20,11 @@ #include "resolve-undo.h" #include "submodule.h" #include "argv-array.h" +#include "sigchain.h" static const char * const checkout_usage[] = { - N_("git checkout [options] <branch>"), - N_("git checkout [options] [<branch>] -- <file>..."), + N_("git checkout [<options>] <branch>"), + N_("git checkout [<options>] [<branch>] -- <file>..."), NULL, }; @@ -36,6 +37,7 @@ struct checkout_opts { int writeout_stage; int overwrite_ignore; int ignore_skipworktree; + int ignore_other_worktrees; const char *new_branch; const char *new_branch_force; @@ -48,6 +50,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, @@ -267,6 +273,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); @@ -441,6 +450,11 @@ struct branch_info { const char *name; /* The short name used */ const char *path; /* The full name of a real branch */ struct commit *commit; /* The named commit */ + /* + * if not null the branch is detached because it's already + * checked out in this checkout + */ + char *checkout; }; static void setup_branch_path(struct branch_info *branch) @@ -502,7 +516,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); @@ -606,18 +620,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts, if (opts->new_orphan_branch) { if (opts->new_branch_log && !log_all_ref_updates) { int temp; - char log_file[PATH_MAX]; - char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch); + struct strbuf log_file = STRBUF_INIT; + int ret; + const char *ref_name; + ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch); temp = log_all_ref_updates; log_all_ref_updates = 1; - if (log_ref_setup(ref_name, log_file, sizeof(log_file))) { + ret = log_ref_setup(ref_name, &log_file); + log_all_ref_updates = temp; + strbuf_release(&log_file); + if (ret) { fprintf(stderr, _("Can not do reflog for '%s'\n"), opts->new_orphan_branch); - log_all_ref_updates = temp; return; } - log_all_ref_updates = temp; } } else @@ -743,10 +760,17 @@ static void suggest_reattach(struct commit *commit, struct rev_info *revs) if (advice_detached_head) fprintf(stderr, - _( + Q_( + /* The singular version */ + "If you want to keep it by creating a new branch, " + "this may be a good time\nto do so with:\n\n" + " git branch <new-branch-name> %s\n\n", + /* The plural version */ "If you want to keep them by creating a new branch, " "this may be a good time\nto do so with:\n\n" - " git branch new_branch_name %s\n\n"), + " git branch <new-branch-name> %s\n\n", + /* Give ngettext() the count */ + lost), find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV)); } @@ -815,7 +839,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); @@ -825,6 +850,138 @@ static int switch_branches(const struct checkout_opts *opts, return ret || writeout_error; } +static char *junk_work_tree; +static char *junk_git_dir; +static int is_junk; +static pid_t junk_pid; + +static void remove_junk(void) +{ + struct strbuf sb = STRBUF_INIT; + if (!is_junk || getpid() != junk_pid) + return; + if (junk_git_dir) { + strbuf_addstr(&sb, junk_git_dir); + remove_dir_recursively(&sb, 0); + strbuf_reset(&sb); + } + if (junk_work_tree) { + strbuf_addstr(&sb, junk_work_tree); + remove_dir_recursively(&sb, 0); + } + strbuf_release(&sb); +} + +static void remove_junk_on_signal(int signo) +{ + remove_junk(); + sigchain_pop(signo); + raise(signo); +} + +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, *name; + struct stat st; + struct child_process cp; + int counter = 0, len, ret; + + if (!new->commit) + die(_("no branch specified")); + if (file_exists(path) && !is_empty_dir(path)) + die(_("'%s' already exists"), path); + + len = strlen(path); + while (len && is_dir_sep(path[len - 1])) + len--; + + for (name = path + len - 1; name > path; name--) + if (is_dir_sep(*name)) { + name++; + break; + } + strbuf_addstr(&sb_repo, + git_path("worktrees/%.*s", (int)(path + len - name), 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 = strrchr(sb_repo.buf, '/') + 1; + + junk_pid = getpid(); + atexit(remove_junk); + sigchain_push_common(remove_junk_on_signal); + + if (mkdir(sb_repo.buf, 0777)) + die_errno(_("could not create directory of '%s'"), sb_repo.buf); + junk_git_dir = xstrdup(sb_repo.buf); + is_junk = 1; + + /* + * lock the incomplete repo so prune won't delete it, unlock + * after the preparation is over. + */ + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + write_file(sb.buf, 1, "initializing\n"); + + 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); + junk_work_tree = xstrdup(path); + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); + write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf)); + write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n", + real_path(get_git_common_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_reset(&sb); + 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; + ret = run_command(&cp); + if (!ret) { + is_junk = 0; + free(junk_work_tree); + free(junk_git_dir); + junk_work_tree = NULL; + junk_git_dir = NULL; + } + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + unlink_or_warn(sb.buf); + strbuf_release(&sb); + strbuf_release(&sb_repo); + strbuf_release(&sb_git); + return ret; +} + static int git_checkout_config(const char *var, const char *value, void *cb) { if (!strcmp(var, "diff.ignoresubmodules")) { @@ -880,13 +1037,80 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1) return NULL; } +static void check_linked_checkout(struct branch_info *new, const char *id) +{ + struct strbuf sb = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + struct strbuf gitdir = STRBUF_INIT; + const char *start, *end; + + if (id) + strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id); + else + strbuf_addf(&path, "%s/HEAD", get_git_common_dir()); + + if (strbuf_read_file(&sb, path.buf, 0) < 0 || + !skip_prefix(sb.buf, "ref:", &start)) + goto done; + while (isspace(*start)) + start++; + end = start; + while (*end && !isspace(*end)) + end++; + if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0') + goto done; + if (id) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id); + if (strbuf_read_file(&gitdir, path.buf, 0) <= 0) + goto done; + strbuf_rtrim(&gitdir); + } else + strbuf_addstr(&gitdir, get_git_common_dir()); + die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf); +done: + strbuf_release(&path); + strbuf_release(&sb); + strbuf_release(&gitdir); +} + +static void check_linked_checkouts(struct branch_info *new) +{ + struct strbuf path = STRBUF_INIT; + DIR *dir; + struct dirent *d; + + strbuf_addf(&path, "%s/worktrees", get_git_common_dir()); + if ((dir = opendir(path.buf)) == NULL) { + strbuf_release(&path); + return; + } + + /* + * $GIT_COMMON_DIR/HEAD is practically outside + * $GIT_DIR so resolve_ref_unsafe() won't work (it + * uses git_path). Parse the ref ourselves. + */ + check_linked_checkout(new, NULL); + + while ((d = readdir(dir)) != NULL) { + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; + check_linked_checkout(new, d->d_name); + } + strbuf_release(&path); + closedir(dir); +} + static int parse_branchname_arg(int argc, const char **argv, int dwim_new_local_branch_ok, struct branch_info *new, - struct tree **source_tree, - unsigned char rev[20], - const char **new_branch) + struct checkout_opts *opts, + unsigned char rev[20]) { + 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; @@ -1007,6 +1231,17 @@ 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 */ @@ -1086,6 +1321,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; @@ -1127,7 +1365,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) OPT_BOOL(0, "ignore-skip-worktree-bits", &opts.ignore_skipworktree, 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'")), + N_("second guess 'git checkout <no-such-branch>'")), + OPT_FILENAME(0, "to", &opts.new_worktree, + N_("check a branch out in a separate working directory")), + OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees, + N_("do not check if another worktree is holding the given ref")), OPT_END(), }; @@ -1136,6 +1378,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); @@ -1144,6 +1389,14 @@ 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 (!opts.new_worktree) + setup_work_tree(); + if (conflict_style) { opts.merge = 1; /* implied */ git_xmerge_config("merge.conflictstyle", conflict_style, NULL); @@ -1197,8 +1450,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) opts.track == BRANCH_TRACK_UNSPECIFIED && !opts.new_branch; int n = parse_branchname_arg(argc, argv, dwim_ok, - &new, &opts.source_tree, - rev, &opts.new_branch); + &new, &opts, rev); argv += n; argc -= n; } |