run test suite without dashed git-commands in PATH
[gitweb.git] / builtin-checkout.c
index c6d6ac99f63459094d3e1a110fed228a74f7927b..64f3a11ae1d415c466def8eccefe9dbc5b6b3926 100644 (file)
@@ -302,8 +302,9 @@ static void show_local_changes(struct object *head)
 static void describe_detached_head(char *msg, struct commit *commit)
 {
        struct strbuf sb = STRBUF_INIT;
+       struct pretty_print_context ctx = {0};
        parse_commit(commit);
-       pretty_print_commit(CMIT_FMT_ONELINE, commit, &sb, 0, NULL, NULL, 0, 0);
+       pretty_print_commit(CMIT_FMT_ONELINE, commit, &sb, &ctx);
        fprintf(stderr, "%s %s... %s\n", msg,
                find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV), sb.buf);
        strbuf_release(&sb);
@@ -566,6 +567,47 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
        return git_xmerge_config(var, value, cb);
 }
 
+static int interactive_checkout(const char *revision, const char **pathspec,
+                               struct checkout_opts *opts)
+{
+       return run_add_interactive(revision, "--patch=checkout", pathspec);
+}
+
+struct tracking_name_data {
+       const char *name;
+       char *remote;
+       int unique;
+};
+
+static int check_tracking_name(const char *refname, const unsigned char *sha1,
+                              int flags, void *cb_data)
+{
+       struct tracking_name_data *cb = cb_data;
+       const char *slash;
+
+       if (prefixcmp(refname, "refs/remotes/"))
+               return 0;
+       slash = strchr(refname + 13, '/');
+       if (!slash || strcmp(slash + 1, cb->name))
+               return 0;
+       if (cb->remote) {
+               cb->unique = 0;
+               return 0;
+       }
+       cb->remote = xstrdup(refname);
+       return 0;
+}
+
+static const char *unique_tracking_name(const char *name)
+{
+       struct tracking_name_data cb_data = { name, NULL, 1 };
+       for_each_ref(check_tracking_name, &cb_data);
+       if (cb_data.unique)
+               return cb_data.remote;
+       free(cb_data.remote);
+       return NULL;
+}
+
 int cmd_checkout(int argc, const char **argv, const char *prefix)
 {
        struct checkout_opts opts;
@@ -574,6 +616,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
        struct branch_info new;
        struct tree *source_tree = NULL;
        char *conflict_style = NULL;
+       int patch_mode = 0;
+       int dwim_new_local_branch = 1;
        struct option options[] = {
                OPT__QUIET(&opts.quiet),
                OPT_STRING('b', NULL, &opts.new_branch, "new branch", "branch"),
@@ -584,10 +628,14 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                            2),
                OPT_SET_INT('3', "theirs", &opts.writeout_stage, "stage",
                            3),
-               OPT_BOOLEAN('f', NULL, &opts.force, "force"),
+               OPT_BOOLEAN('f', "force", &opts.force, "force"),
                OPT_BOOLEAN('m', "merge", &opts.merge, "merge"),
                OPT_STRING(0, "conflict", &conflict_style, "style",
                           "conflict style (merge or diff3)"),
+               OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
+               { OPTION_BOOLEAN, 0, "guess", &dwim_new_local_branch, NULL,
+                 "second guess 'git checkout no-such-branch'",
+                 PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },
                OPT_END(),
        };
        int has_dash_dash;
@@ -602,6 +650,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 (patch_mode && (opts.track > 0 || opts.new_branch
+                          || opts.new_branch_log || opts.merge || opts.force))
+               die ("--patch is incompatible with all other options");
+
        /* --track without -b should DWIM */
        if (0 < opts.track && !opts.new_branch) {
                const char *argv0 = argv[0];
@@ -617,8 +669,6 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                opts.new_branch = argv0 + 1;
        }
 
-       if (opts.track == BRANCH_TRACK_UNSPECIFIED)
-               opts.track = git_branch_track;
        if (conflict_style) {
                opts.merge = 1; /* implied */
                git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
@@ -642,6 +692,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
         *   With no paths, if <something> is a commit, that is to
         *   switch to the branch or detach HEAD at it.
         *
+        *   With no paths, if <something> is _not_ a commit, no -t nor -b
+        *   was given, and there is a tracking branch whose name is
+        *   <something> in one and only one remote, then this is a short-hand
+        *   to fork local <something> from that remote tracking branch.
+        *
         *   Otherwise <something> shall not be ambiguous.
         *   - If it's *only* a reference, treat it like case (1).
         *   - If it's only a path, treat it like case (2).
@@ -664,7 +719,21 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                if (get_sha1(arg, rev)) {
                        if (has_dash_dash)          /* case (1) */
                                die("invalid reference: %s", arg);
-                       goto no_reference;          /* case (3 -> 2) */
+                       if (!patch_mode &&
+                           dwim_new_local_branch &&
+                           opts.track == BRANCH_TRACK_UNSPECIFIED &&
+                           !opts.new_branch &&
+                           !check_filename(NULL, arg) &&
+                           argc == 1) {
+                               const char *remote = unique_tracking_name(arg);
+                               if (!remote || get_sha1(remote, rev))
+                                       goto no_reference;
+                               opts.new_branch = arg;
+                               arg = remote;
+                               /* DWIMmed to create local branch */
+                       }
+                       else
+                               goto no_reference;
                }
 
                /* we can't end up being in (2) anymore, eat the argument */
@@ -702,12 +771,19 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
        }
 
 no_reference:
+
+       if (opts.track == BRANCH_TRACK_UNSPECIFIED)
+               opts.track = git_branch_track;
+
        if (argc) {
                const char **pathspec = get_pathspec(prefix, argv);
 
                if (!pathspec)
                        die("invalid path specification");
 
+               if (patch_mode)
+                       return interactive_checkout(new.name, pathspec, &opts);
+
                /* Checkout paths */
                if (opts.new_branch) {
                        if (argc == 1) {
@@ -723,6 +799,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                return checkout_paths(source_tree, pathspec, &opts);
        }
 
+       if (patch_mode)
+               return interactive_checkout(new.name, NULL, &opts);
+
        if (opts.new_branch) {
                struct strbuf buf = STRBUF_INIT;
                if (strbuf_check_branch_ref(&buf, opts.new_branch))