Merge branch 'uk/checkout-ambiguous-ref'
authorJunio C Hamano <gitster@pobox.com>
Mon, 28 Feb 2011 05:58:29 +0000 (21:58 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 28 Feb 2011 05:58:29 +0000 (21:58 -0800)
* uk/checkout-ambiguous-ref:
Rename t2019 with typo "amiguous" that meant "ambiguous"
checkout: rearrange update_refs_for_switch for clarity
checkout: introduce --detach synonym for "git checkout foo^{commit}"
checkout: split off a function to peel away branchname arg
checkout: fix bug with ambiguous refs

Conflicts:
builtin/checkout.c

1  2 
Documentation/git-checkout.txt
builtin/checkout.c
index 880763d391d18364485edc2e1e3dfc6f52243973,d162117e7156fdf18754261b22cb024475960f46..87863fcadc74646689be18d401339372769f16fd
@@@ -9,6 -9,7 +9,7 @@@ SYNOPSI
  --------
  [verse]
  'git checkout' [-q] [-f] [-m] [<branch>]
+ 'git checkout' [-q] [-f] [-m] [--detach] [<commit>]
  'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
  'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
  'git checkout' --patch [<tree-ish>] [--] [<paths>...]
@@@ -22,9 -23,10 +23,10 @@@ branch
  
  'git checkout' [<branch>]::
  'git checkout' -b|-B <new_branch> [<start point>]::
+ 'git checkout' [--detach] [<commit>]::
  
        This form switches branches by updating the index, working
-       tree, and HEAD to reflect the specified branch.
+       tree, and HEAD to reflect the specified branch or commit.
  +
  If `-b` is given, a new branch is created as if linkgit:git-branch[1]
  were called and then checked out; in this case you can
@@@ -98,7 -100,7 +100,7 @@@ entries; instead, unmerged entries are 
        "--track" in linkgit:git-branch[1] for details.
  +
  If no '-b' option is given, the name of the new branch will be
 -derived from the remote branch.  If "remotes/" or "refs/remotes/"
 +derived from the remote-tracking branch.  If "remotes/" or "refs/remotes/"
  is prefixed it is stripped away, and then the part up to the
  next slash (which would be the nickname of the remote) is removed.
  This would tell us to use "hack" as the local branch when branching
@@@ -115,6 -117,13 +117,13 @@@ explicitly give a name with '-b' in suc
        Create the new branch's reflog; see linkgit:git-branch[1] for
        details.
  
+ --detach::
+       Rather than checking out a branch to work on it, check out a
+       commit for inspection and discardable experiments.
+       This is the default behavior of "git checkout <commit>" when
+       <commit> is not a branch name.  See the "DETACHED HEAD" section
+       below for details.
  --orphan::
        Create a new 'orphan' branch, named <new_branch>, started from
        <start_point> and switch to it.  The first commit made on this
@@@ -204,7 -213,7 +213,7 @@@ leave out at most one of `A` and `B`, i
  
  
  
- Detached HEAD
+ DETACHED HEAD
  -------------
  
  It is sometimes useful to be able to 'checkout' a commit that is
diff --combined builtin/checkout.c
index bef324e4717fbe73da852c49c664e02d70955fc9,179d047b20787979a2266527caceb44342056092..cc97dbc30f1b064a44157897d9e325d934111cee
@@@ -30,6 -30,7 +30,7 @@@ struct checkout_opts 
        int quiet;
        int merge;
        int force;
+       int force_detach;
        int writeout_stage;
        int writeout_error;
  
@@@ -161,7 -162,7 +162,7 @@@ static int checkout_merged(int pos, str
         * merge.renormalize set, too
         */
        status = ll_merge(&result_buf, path, &ancestor, "base",
 -                        &ours, "ours", &theirs, "theirs", 0);
 +                        &ours, "ours", &theirs, "theirs", NULL);
        free(ancestor.ptr);
        free(ours.ptr);
        free(theirs.ptr);
@@@ -297,7 -298,7 +298,7 @@@ static void show_local_changes(struct o
        run_diff_index(&rev, 0);
  }
  
 -static void describe_detached_head(char *msg, struct commit *commit)
 +static void describe_detached_head(const char *msg, struct commit *commit)
  {
        struct strbuf sb = STRBUF_INIT;
        struct pretty_print_context ctx = {0};
@@@ -404,7 -405,7 +405,7 @@@ static int merge_working_tree(struct ch
                topts.dir->exclude_per_dir = ".gitignore";
                tree = parse_tree_indirect(old->commit ?
                                           old->commit->object.sha1 :
 -                                         (unsigned char *)EMPTY_TREE_SHA1_BIN);
 +                                         EMPTY_TREE_SHA1_BIN);
                init_tree_desc(&trees[0], tree->buffer, tree->size);
                tree = parse_tree_indirect(new->commit->object.sha1);
                init_tree_desc(&trees[1], tree->buffer, tree->size);
@@@ -541,7 -542,17 +542,17 @@@ static void update_refs_for_switch(stru
        strbuf_addf(&msg, "checkout: moving from %s to %s",
                    old_desc ? old_desc : "(invalid)", new->name);
  
-       if (new->path) {
+       if (!strcmp(new->name, "HEAD") && !new->path && !opts->force_detach) {
+               /* Nothing to do. */
+       } else if (opts->force_detach || !new->path) {  /* No longer on any branch. */
+               update_ref(msg.buf, "HEAD", new->commit->object.sha1, NULL,
+                          REF_NODEREF, DIE_ON_ERR);
+               if (!opts->quiet) {
+                       if (old->path && advice_detached_head)
+                               detach_advice(old->path, new->name);
+                       describe_detached_head("HEAD is now at", new->commit);
+               }
+       } else if (new->path) { /* Switch branches. */
                create_symref("HEAD", new->path, msg.buf);
                if (!opts->quiet) {
                        if (old->path && !strcmp(new->path, old->path))
                        if (!file_exists(ref_file) && file_exists(log_file))
                                remove_path(log_file);
                }
-       } else if (strcmp(new->name, "HEAD")) {
-               update_ref(msg.buf, "HEAD", new->commit->object.sha1, NULL,
-                          REF_NODEREF, DIE_ON_ERR);
-               if (!opts->quiet) {
-                       if (old->path && advice_detached_head)
-                               detach_advice(old->path, new->name);
-                       describe_detached_head("HEAD is now at", new->commit);
-               }
        }
        remove_branch_state();
        strbuf_release(&msg);
-       if (!opts->quiet && (new->path || !strcmp(new->name, "HEAD")))
+       if (!opts->quiet &&
+           (new->path || (!opts->force_detach && !strcmp(new->name, "HEAD"))))
                report_tracking(new);
  }
  
@@@ -675,23 -679,136 +679,136 @@@ static const char *unique_tracking_name
        return NULL;
  }
  
 -       *   to fork local <something> from that remote tracking branch.
+ 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)
+ {
+       int argcount = 0;
+       unsigned char branch_rev[20];
+       const char *arg;
+       int has_dash_dash;
+       /*
+        * case 1: git checkout <ref> -- [<paths>]
+        *
+        *   <ref> must be a valid tree, everything after the '--' must be
+        *   a path.
+        *
+        * case 2: git checkout -- [<paths>]
+        *
+        *   everything after the '--' must be paths.
+        *
+        * case 3: git checkout <something> [<paths>]
+        *
+        *   With no paths, if <something> is a commit, that is to
+        *   switch to the branch or detach HEAD at it.  As a special case,
+        *   if <something> is A...B (missing A or B means HEAD but you can
+        *   omit at most one side), and if there is a unique merge base
+        *   between A and B, A...B names that merge base.
+        *
+        *   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).
+        *   - else: fail.
+        *
+        */
+       if (!argc)
+               return 0;
+       if (!strcmp(argv[0], "--"))     /* case (2) */
+               return 1;
+       arg = argv[0];
+       has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
+       if (!strcmp(arg, "-"))
+               arg = "@{-1}";
+       if (get_sha1_mb(arg, rev)) {
+               if (has_dash_dash)          /* case (1) */
+                       die("invalid reference: %s", arg);
+               if (dwim_new_local_branch_ok &&
+                   !check_filename(NULL, arg) &&
+                   argc == 1) {
+                       const char *remote = unique_tracking_name(arg);
+                       if (!remote || get_sha1(remote, rev))
+                               return argcount;
+                       *new_branch = arg;
+                       arg = remote;
+                       /* DWIMmed to create local branch */
+               } else {
+                       return argcount;
+               }
+       }
+       /* we can't end up being in (2) anymore, eat the argument */
+       argcount++;
+       argv++;
+       argc--;
+       new->name = arg;
+       setup_branch_path(new);
+       if (check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&
+           resolve_ref(new->path, branch_rev, 1, NULL))
+               hashcpy(rev, branch_rev);
+       else
+               new->path = NULL; /* not an existing branch */
+       new->commit = lookup_commit_reference_gently(rev, 1);
+       if (!new->commit) {
+               /* not a commit */
+               *source_tree = parse_tree_indirect(rev);
+       } else {
+               parse_commit(new->commit);
+               *source_tree = new->commit->tree;
+       }
+       if (!*source_tree)                   /* case (1): want a tree */
+               die("reference is not a tree: %s", arg);
+       if (!has_dash_dash) {/* case (3 -> 1) */
+               /*
+                * Do not complain the most common case
+                *      git checkout branch
+                * even if there happen to be a file called 'branch';
+                * it would be extremely annoying.
+                */
+               if (argc)
+                       verify_non_filename(NULL, arg);
+       } else {
+               argcount++;
+               argv++;
+               argc--;
+       }
+       return argcount;
+ }
  int cmd_checkout(int argc, const char **argv, const char *prefix)
  {
        struct checkout_opts opts;
        unsigned char rev[20];
-       const char *arg;
        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__QUIET(&opts.quiet, "suppress progress reporting"),
                OPT_STRING('b', NULL, &opts.new_branch, "branch",
                           "create and checkout a new branch"),
                OPT_STRING('B', NULL, &opts.new_branch_force, "branch",
                           "create/reset and checkout a branch"),
                OPT_BOOLEAN('l', NULL, &opts.new_branch_log, "create reflog for new branch"),
+               OPT_BOOLEAN(0, "detach", &opts.force_detach, "detach the HEAD at named commit"),
                OPT_SET_INT('t', "track",  &opts.track, "set upstream info for new branch",
                        BRANCH_TRACK_EXPLICIT),
                OPT_STRING(0, "orphan", &opts.new_orphan_branch, "new branch", "new unparented branch"),
                            2),
                OPT_SET_INT('3', "theirs", &opts.writeout_stage, "checkout their version for unmerged files",
                            3),
 -              OPT_BOOLEAN('f', "force", &opts.force, "force checkout (throw away local modifications)"),
 +              OPT__FORCE(&opts.force, "force checkout (throw away local modifications)"),
                OPT_BOOLEAN('m', "merge", &opts.merge, "perform a 3-way merge with the new branch"),
                OPT_STRING(0, "conflict", &conflict_style, "style",
                           "conflict style (merge or diff3)"),
                  PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },
                OPT_END(),
        };
-       int has_dash_dash;
  
        memset(&opts, 0, sizeof(opts));
        memset(&new, 0, sizeof(new));
                opts.new_branch = opts.new_branch_force;
  
        if (patch_mode && (opts.track > 0 || opts.new_branch
-                          || opts.new_branch_log || opts.merge || opts.force))
+                          || opts.new_branch_log || opts.merge || opts.force
+                          || opts.force_detach))
                die ("--patch is incompatible with all other options");
  
+       if (opts.force_detach && (opts.new_branch || opts.new_orphan_branch))
+               die("--detach cannot be used with -b/-B/--orphan");
+       if (opts.force_detach && 0 < opts.track)
+               die("--detach cannot be used with -t");
        /* --track without -b should DWIM */
        if (0 < opts.track && !opts.new_branch) {
                const char *argv0 = argv[0];
                die("git checkout: -f and -m are incompatible");
  
        /*
-        * case 1: git checkout <ref> -- [<paths>]
-        *
-        *   <ref> must be a valid tree, everything after the '--' must be
-        *   a path.
-        *
-        * case 2: git checkout -- [<paths>]
-        *
-        *   everything after the '--' must be paths.
-        *
-        * case 3: git checkout <something> [<paths>]
-        *
-        *   With no paths, if <something> is a commit, that is to
-        *   switch to the branch or detach HEAD at it.  As a special case,
-        *   if <something> is A...B (missing A or B means HEAD but you can
-        *   omit at most one side), and if there is a unique merge base
-        *   between A and B, A...B names that merge base.
+        * Extract branch name from command line arguments, so
+        * all that is left is pathspecs.
         *
-        *   With no paths, if <something> is _not_ a commit, no -t nor -b
-        *   was given, and there is a remote-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.
+        * Handle
         *
-        *   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).
-        *   - else: fail.
+        *  1) git checkout <tree> -- [<paths>]
+        *  2) git checkout -- [<paths>]
+        *  3) git checkout <something> [<paths>]
         *
+        * including "last branch" syntax and DWIM-ery for names of
+        * remote branches, erroring out for invalid or ambiguous cases.
         */
        if (argc) {
-               if (!strcmp(argv[0], "--")) {       /* case (2) */
-                       argv++;
-                       argc--;
-                       goto no_reference;
-               }
-               arg = argv[0];
-               has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
-               if (!strcmp(arg, "-"))
-                       arg = "@{-1}";
-               if (get_sha1_mb(arg, rev)) {
-                       if (has_dash_dash)          /* case (1) */
-                               die("invalid reference: %s", arg);
-                       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 */
-               argv++;
-               argc--;
-               new.name = arg;
-               if ((new.commit = lookup_commit_reference_gently(rev, 1))) {
-                       setup_branch_path(&new);
-                       if ((check_ref_format(new.path) == CHECK_REF_FORMAT_OK) &&
-                           resolve_ref(new.path, rev, 1, NULL))
-                               ;
-                       else
-                               new.path = NULL;
-                       parse_commit(new.commit);
-                       source_tree = new.commit->tree;
-               } else
-                       source_tree = parse_tree_indirect(rev);
-               if (!source_tree)                   /* case (1): want a tree */
-                       die("reference is not a tree: %s", arg);
-               if (!has_dash_dash) {/* case (3 -> 1) */
-                       /*
-                        * Do not complain the most common case
-                        *      git checkout branch
-                        * even if there happen to be a file called 'branch';
-                        * it would be extremely annoying.
-                        */
-                       if (argc)
-                               verify_non_filename(NULL, arg);
-               }
-               else {
-                       argv++;
-                       argc--;
-               }
+               int dwim_ok =
+                       !patch_mode &&
+                       dwim_new_local_branch &&
+                       opts.track == BRANCH_TRACK_UNSPECIFIED &&
+                       !opts.new_branch;
+               int n = parse_branchname_arg(argc, argv, dwim_ok,
+                               &new, &source_tree, rev, &opts.new_branch);
+               argv += n;
+               argc -= n;
        }
  
- no_reference:
        if (opts.track == BRANCH_TRACK_UNSPECIFIED)
                opts.track = git_branch_track;
  
                        }
                }
  
+               if (opts.force_detach)
+                       die("git checkout: --detach does not take a path argument");
                if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
                        die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index.");