Merge branch 'jc/checkout-merge-base'
authorJunio C Hamano <gitster@pobox.com>
Wed, 13 Jan 2010 20:31:13 +0000 (12:31 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 13 Jan 2010 20:31:13 +0000 (12:31 -0800)
* jc/checkout-merge-base:
  rebase -i: teach --onto A...B syntax
  rebase: fix --onto A...B parsing and add tests
  "rebase --onto A...B" replays history on the merge base between A and B
  "checkout A...B" switches to the merge base between A and B

1  2 
builtin-checkout.c
cache.h
git-rebase--interactive.sh
git-rebase.sh
sha1_name.c

diff --combined builtin-checkout.c
@@@ -302,9 -302,8 +302,9 @@@ static void show_local_changes(struct o
  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);
@@@ -397,7 -396,7 +397,7 @@@ static int merge_working_tree(struct ch
                topts.initial_checkout = is_cache_unborn();
                topts.update = 1;
                topts.merge = 1;
 -              topts.gently = opts->merge;
 +              topts.gently = opts->merge && old->commit;
                topts.verbose_update = !opts->quiet;
                topts.fn = twoway_merge;
                topts.dir = xcalloc(1, sizeof(*topts.dir));
                        struct merge_options o;
                        if (!opts->merge)
                                return 1;
 -                      parse_commit(old->commit);
 +
 +                      /*
 +                       * Without old->commit, the below is the same as
 +                       * the two-tree unpack we already tried and failed.
 +                       */
 +                      if (!old->commit)
 +                              return 1;
  
                        /* Do more real merge */
  
@@@ -696,7 -689,10 +696,10 @@@ int cmd_checkout(int argc, const char *
         * 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.
+        *   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
                if (!strcmp(arg, "-"))
                        arg = "@{-1}";
  
-               if (get_sha1(arg, rev)) {
+               if (get_sha1_mb(arg, rev)) {
                        if (has_dash_dash)          /* case (1) */
                                die("invalid reference: %s", arg);
                        if (!patch_mode &&
diff --combined cache.h
+++ b/cache.h
@@@ -177,20 -177,15 +177,20 @@@ struct cache_entry 
  
  #define CE_HASHED    (0x100000)
  #define CE_UNHASHED  (0x200000)
 +#define CE_CONFLICTED (0x800000)
 +
 +/* Only remove in work directory, not index */
 +#define CE_WT_REMOVE (0x400000)
  
  /*
   * Extended on-disk flags
   */
  #define CE_INTENT_TO_ADD 0x20000000
 +#define CE_SKIP_WORKTREE 0x40000000
  /* CE_EXTENDED2 is for future extension */
  #define CE_EXTENDED2 0x80000000
  
 -#define CE_EXTENDED_FLAGS (CE_INTENT_TO_ADD)
 +#define CE_EXTENDED_FLAGS (CE_INTENT_TO_ADD | CE_SKIP_WORKTREE)
  
  /*
   * Safeguard to avoid saving wrong flags:
@@@ -239,7 -234,6 +239,7 @@@ static inline size_t ce_namelen(const s
                            ondisk_cache_entry_size(ce_namelen(ce)))
  #define ce_stage(ce) ((CE_STAGEMASK & (ce)->ce_flags) >> CE_STAGESHIFT)
  #define ce_uptodate(ce) ((ce)->ce_flags & CE_UPTODATE)
 +#define ce_skip_worktree(ce) ((ce)->ce_flags & CE_SKIP_WORKTREE)
  #define ce_mark_uptodate(ce) ((ce)->ce_flags |= CE_UPTODATE)
  
  #define ce_permissions(mode) (((mode) & 0100) ? 0755 : 0644)
@@@ -375,12 -369,9 +375,12 @@@ static inline enum object_type object_t
  #define CONFIG_ENVIRONMENT "GIT_CONFIG"
  #define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH"
  #define CEILING_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
 +#define NO_REPLACE_OBJECTS_ENVIRONMENT "GIT_NO_REPLACE_OBJECTS"
  #define GITATTRIBUTES_FILE ".gitattributes"
  #define INFOATTRIBUTES_FILE "info/attributes"
  #define ATTRIBUTE_MACRO_PREFIX "[attr]"
 +#define GIT_NOTES_REF_ENVIRONMENT "GIT_NOTES_REF"
 +#define GIT_NOTES_DEFAULT_REF "refs/notes/commits"
  
  extern int is_bare_repository_cfg;
  extern int is_bare_repository(void);
@@@ -470,9 -461,7 +470,9 @@@ extern int index_name_is_other(const st
  /* do stat comparison even if CE_VALID is true */
  #define CE_MATCH_IGNORE_VALID         01
  /* do not check the contents but report dirty on racily-clean entries */
 -#define CE_MATCH_RACY_IS_DIRTY        02
 +#define CE_MATCH_RACY_IS_DIRTY                02
 +/* do stat comparison even if CE_SKIP_WORKTREE is true */
 +#define CE_MATCH_IGNORE_SKIP_WORKTREE 04
  extern int ie_match_stat(const struct index_state *, struct cache_entry *, struct stat *, unsigned int);
  extern int ie_modified(const struct index_state *, struct cache_entry *, struct stat *, unsigned int);
  
@@@ -537,7 -526,6 +537,7 @@@ extern int auto_crlf
  extern int read_replace_refs;
  extern int fsync_object_files;
  extern int core_preload_index;
 +extern int core_apply_sparse_checkout;
  
  enum safe_crlf {
        SAFE_CRLF_FALSE = 0,
@@@ -580,8 -568,6 +580,8 @@@ enum object_creation_mode 
  
  extern enum object_creation_mode object_creation_mode;
  
 +extern char *notes_ref_name;
 +
  extern int grafts_replace_parents;
  
  #define GIT_REPO_VERSION 0
@@@ -659,7 -645,6 +659,7 @@@ int set_shared_perm(const char *path, i
  #define adjust_shared_perm(path) set_shared_perm((path), 0)
  int safe_create_leading_directories(char *path);
  int safe_create_leading_directories_const(const char *path);
 +extern char *expand_user_path(const char *path);
  char *enter_repo(char *path, int strict);
  static inline int is_absolute_path(const char *path)
  {
@@@ -672,7 -657,6 +672,7 @@@ const char *make_relative_path(const ch
  int normalize_path_copy(char *dst, const char *src);
  int longest_ancestor_length(const char *path, const char *prefix_list);
  char *strip_path_suffix(const char *path, const char *suffix);
 +int daemon_avoid_alias(const char *path);
  
  /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
  extern int sha1_object_info(const unsigned char *, unsigned long *);
@@@ -711,11 -695,7 +711,11 @@@ static inline unsigned int hexval(unsig
  #define DEFAULT_ABBREV 7
  
  extern int get_sha1(const char *str, unsigned char *sha1);
 -extern int get_sha1_with_mode(const char *str, unsigned char *sha1, unsigned *mode);
 +extern int get_sha1_with_mode_1(const char *str, unsigned char *sha1, unsigned *mode, int gently, const char *prefix);
 +static inline int get_sha1_with_mode(const char *str, unsigned char *sha1, unsigned *mode)
 +{
 +      return get_sha1_with_mode_1(str, sha1, mode, 1, NULL);
 +}
  extern int get_sha1_hex(const char *hex, unsigned char *sha1);
  extern char *sha1_to_hex(const unsigned char *sha1);  /* static buffer result! */
  extern int read_ref(const char *filename, unsigned char *sha1);
@@@ -723,6 -703,7 +723,7 @@@ extern const char *resolve_ref(const ch
  extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
  extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
  extern int interpret_branch_name(const char *str, struct strbuf *);
+ extern int get_sha1_mb(const char *str, unsigned char *sha1);
  
  extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
  extern const char *ref_rev_parse_rules[];
@@@ -771,8 -752,6 +772,8 @@@ extern const char *git_author_info(int)
  extern const char *git_committer_info(int);
  extern const char *fmt_ident(const char *name, const char *email, const char *date_str, int);
  extern const char *fmt_name(const char *name, const char *email);
 +extern const char *git_editor(void);
 +extern const char *git_pager(void);
  
  struct checkout {
        const char *base_dir;
@@@ -879,6 -858,7 +880,6 @@@ extern struct ref *find_ref_by_name(con
  extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags);
  extern int finish_connect(struct child_process *conn);
  extern int path_match(const char *path, int nr, char **match);
 -extern int get_ack(int fd, unsigned char *result_sha1);
  struct extra_have_objects {
        int nr, alloc;
        unsigned char (*array)[20];
@@@ -924,7 -904,6 +925,7 @@@ extern unsigned long git_config_ulong(c
  extern int git_config_bool_or_int(const char *, const char *, int *);
  extern int git_config_bool(const char *, const char *);
  extern int git_config_string(const char **, const char *, const char *);
 +extern int git_config_pathname(const char **, const char *, const char *);
  extern int git_config_set(const char *, const char *);
  extern int git_config_set_multivar(const char *, const char *, const char *, int);
  extern int git_config_rename_section(const char *, const char *);
@@@ -983,9 -962,7 +984,9 @@@ extern void *alloc_object_node(void)
  extern void alloc_report(void);
  
  /* trace.c */
 +__attribute__((format (printf, 1, 2)))
  extern void trace_printf(const char *format, ...);
 +__attribute__((format (printf, 2, 3)))
  extern void trace_argv_printf(const char **argv, const char *format, ...);
  
  /* convert.c */
@@@ -106,8 -106,8 +106,8 @@@ mark_action_done () 
        sed -e 1q < "$TODO" >> "$DONE"
        sed -e 1d < "$TODO" >> "$TODO".new
        mv -f "$TODO".new "$TODO"
 -      count=$(grep -c '^[^#]' < "$DONE")
 -      total=$(($count+$(grep -c '^[^#]' < "$TODO")))
 +      count=$(sane_grep -c '^[^#]' < "$DONE")
 +      total=$(($count+$(sane_grep -c '^[^#]' < "$TODO")))
        if test "$last_count" != "$count"
        then
                last_count=$count
@@@ -147,7 -147,7 +147,7 @@@ die_abort () 
  }
  
  has_action () {
 -      grep '^[^#]' "$1" >/dev/null
 +      sane_grep '^[^#]' "$1" >/dev/null
  }
  
  pick_one () {
                output git reset --hard $sha1
                test "a$1" = a-n && output git reset --soft $current_sha1
                sha1=$(git rev-parse --short $sha1)
 -              output warn Fast forward to $sha1
 +              output warn Fast-forward to $sha1
        else
                output git cherry-pick "$@"
        fi
@@@ -248,9 -248,9 +248,9 @@@ pick_one_preserving_merges () 
        done
        case $fast_forward in
        t)
 -              output warn "Fast forward to $sha1"
 +              output warn "Fast-forward to $sha1"
                output git reset --hard $sha1 ||
 -                      die "Cannot fast forward to $sha1"
 +                      die "Cannot fast-forward to $sha1"
                ;;
        f)
                first_parent=$(expr "$new_parents" : ' \([^ ]*\)')
@@@ -322,7 -322,7 +322,7 @@@ make_squash_message () 
  }
  
  peek_next_command () {
 -      sed -n "1s/ .*$//p" < "$TODO"
 +      sed -n -e "/^#/d" -e "/^$/d" -e "s/ .*//p" -e "q" < "$TODO"
  }
  
  do_next () {
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
                ;;
 +      reword|r)
 +              comment_for_reflog reword
 +
 +              mark_action_done
 +              pick_one $sha1 ||
 +                      die_with_patch $sha1 "Could not apply $sha1... $rest"
 +              git commit --amend
 +              ;;
        edit|e)
                comment_for_reflog edit
  
                ;;
        *)
                warn "Unknown command: $command $sha1 $rest"
 -              die_with_patch $sha1 "Please fix this in the file $TODO."
 +              if git rev-parse --verify -q "$sha1" >/dev/null
 +              then
 +                      die_with_patch $sha1 "Please fix this in the file $TODO."
 +              else
 +                      die "Please fix this in the file $TODO."
 +              fi
                ;;
        esac
        test -s "$TODO" && return
@@@ -495,6 -482,25 +495,25 @@@ get_saved_options () 
        test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
  }
  
+ LF='
+ '
+ parse_onto () {
+       case "$1" in
+       *...*)
+               if      left=${1%...*} right=${1#*...} &&
+                       onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
+               then
+                       case "$onto" in
+                       ?*"$LF"?* | '')
+                               exit 1 ;;
+                       esac
+                       echo "$onto"
+                       exit 0
+               fi
+       esac
+       git rev-parse --verify "$1^0"
+ }
  while test $# != 0
  do
        case "$1" in
@@@ -602,7 -608,7 +621,7 @@@ first and then run 'git rebase --contin
                ;;
        --onto)
                shift
-               ONTO=$(git rev-parse --verify "$1") ||
+               ONTO=$(parse_onto "$1") ||
                        die "Does not point to a valid commit: $1"
                ;;
        --)
                        git rev-list $REVISIONS |
                        while read rev
                        do
 -                              if test -f "$REWRITTEN"/$rev -a "$(grep "$rev" "$DOTEST"/not-cherry-picks)" = ""
 +                              if test -f "$REWRITTEN"/$rev -a "$(sane_grep "$rev" "$DOTEST"/not-cherry-picks)" = ""
                                then
                                        # Use -f2 because if rev-list is telling us this commit is
                                        # not worthwhile, we don't want to track its multiple heads,
                                        # be rebasing on top of it
                                        git rev-list --parents -1 $rev | cut -d' ' -s -f2 > "$DROPPED"/$rev
                                        short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev)
 -                                      grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO"
 +                                      sane_grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO"
                                        rm "$REWRITTEN"/$rev
                                fi
                        done
  #
  # Commands:
  #  p, pick = use commit
 +#  r, reword = use commit, but edit the commit message
  #  e, edit = use commit, but stop for amending
  #  s, squash = use commit, but meld into previous commit
  #
@@@ -779,7 -784,7 +798,7 @@@ EO
  
                cp "$TODO" "$TODO".backup
                git_editor "$TODO" ||
 -                      die "Could not execute editor"
 +                      die_abort "Could not execute editor"
  
                has_action "$TODO" ||
                        die_abort "Nothing to do"
diff --combined git-rebase.sh
@@@ -34,6 -34,8 +34,8 @@@ set_reflog_action rebas
  require_work_tree
  cd_to_toplevel
  
+ LF='
+ '
  OK_TO_SKIP_PRE_REBASE=
  RESOLVEMSG="
  When you have resolved this problem run \"git rebase --continue\".
  
  # Make sure the branch to rebase onto is valid.
  onto_name=${newbase-"$upstream_name"}
- onto=$(git rev-parse --verify "${onto_name}^0") || exit
+ case "$onto_name" in
+ *...*)
+       if      left=${onto_name%...*} right=${onto_name#*...} &&
+               onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
+       then
+               case "$onto" in
+               ?*"$LF"?*)
+                       die "$onto_name: there are more than one merge bases"
+                       ;;
+               '')
+                       die "$onto_name: there is no merge base"
+                       ;;
+               esac
+       else
+               die "$onto_name: there is no merge base"
+       fi
+       ;;
+ *)
+       onto=$(git rev-parse --verify "${onto_name}^0") || exit
+       ;;
+ esac
  
  # If a hook exists, give it a chance to interrupt
  run_pre_rebase_hook "$upstream_arg" "$@"
@@@ -467,7 -489,7 +489,7 @@@ orig_head=$branc
  mb=$(git merge-base "$onto" "$branch")
  if test "$upstream" = "$onto" && test "$mb" = "$onto" &&
        # linear history?
 -      ! (git rev-list --parents "$onto".."$branch" | grep " .* ") > /dev/null
 +      ! (git rev-list --parents "$onto".."$branch" | sane_grep " .* ") > /dev/null
  then
        if test -z "$force_rebase"
        then
@@@ -496,7 -518,7 +518,7 @@@ the
  fi
  
  # If the $onto is a proper descendant of the tip of the branch, then
 -# we just fast forwarded.
 +# we just fast-forwarded.
  if test "$mb" = "$branch"
  then
        say "Fast-forwarded $branch_name to $onto_name."
diff --combined sha1_name.c
@@@ -794,6 -794,48 +794,48 @@@ release_return
        return retval;
  }
  
+ int get_sha1_mb(const char *name, unsigned char *sha1)
+ {
+       struct commit *one, *two;
+       struct commit_list *mbs;
+       unsigned char sha1_tmp[20];
+       const char *dots;
+       int st;
+       dots = strstr(name, "...");
+       if (!dots)
+               return get_sha1(name, sha1);
+       if (dots == name)
+               st = get_sha1("HEAD", sha1_tmp);
+       else {
+               struct strbuf sb;
+               strbuf_init(&sb, dots - name);
+               strbuf_add(&sb, name, dots - name);
+               st = get_sha1(sb.buf, sha1_tmp);
+               strbuf_release(&sb);
+       }
+       if (st)
+               return st;
+       one = lookup_commit_reference_gently(sha1_tmp, 0);
+       if (!one)
+               return -1;
+       if (get_sha1(dots[3] ? (dots + 3) : "HEAD", sha1_tmp))
+               return -1;
+       two = lookup_commit_reference_gently(sha1_tmp, 0);
+       if (!two)
+               return -1;
+       mbs = get_merge_bases(one, two, 1);
+       if (!mbs || mbs->next)
+               st = -1;
+       else {
+               st = 0;
+               hashcpy(sha1, mbs->item->object.sha1);
+       }
+       free_commit_list(mbs);
+       return st;
+ }
  /*
   * This is like "get_sha1_basic()", except it allows "sha1 expressions",
   * notably "xyz^" for "parent of xyz"
@@@ -804,96 -846,7 +846,96 @@@ int get_sha1(const char *name, unsigne
        return get_sha1_with_mode(name, sha1, &unused);
  }
  
 -int get_sha1_with_mode(const char *name, unsigned char *sha1, unsigned *mode)
 +/* Must be called only when object_name:filename doesn't exist. */
 +static void diagnose_invalid_sha1_path(const char *prefix,
 +                                     const char *filename,
 +                                     const unsigned char *tree_sha1,
 +                                     const char *object_name)
 +{
 +      struct stat st;
 +      unsigned char sha1[20];
 +      unsigned mode;
 +
 +      if (!prefix)
 +              prefix = "";
 +
 +      if (!lstat(filename, &st))
 +              die("Path '%s' exists on disk, but not in '%s'.",
 +                  filename, object_name);
 +      if (errno == ENOENT || errno == ENOTDIR) {
 +              char *fullname = xmalloc(strlen(filename)
 +                                           + strlen(prefix) + 1);
 +              strcpy(fullname, prefix);
 +              strcat(fullname, filename);
 +
 +              if (!get_tree_entry(tree_sha1, fullname,
 +                                  sha1, &mode)) {
 +                      die("Path '%s' exists, but not '%s'.\n"
 +                          "Did you mean '%s:%s'?",
 +                          fullname,
 +                          filename,
 +                          object_name,
 +                          fullname);
 +              }
 +              die("Path '%s' does not exist in '%s'",
 +                  filename, object_name);
 +      }
 +}
 +
 +/* Must be called only when :stage:filename doesn't exist. */
 +static void diagnose_invalid_index_path(int stage,
 +                                      const char *prefix,
 +                                      const char *filename)
 +{
 +      struct stat st;
 +      struct cache_entry *ce;
 +      int pos;
 +      unsigned namelen = strlen(filename);
 +      unsigned fullnamelen;
 +      char *fullname;
 +
 +      if (!prefix)
 +              prefix = "";
 +
 +      /* Wrong stage number? */
 +      pos = cache_name_pos(filename, namelen);
 +      if (pos < 0)
 +              pos = -pos - 1;
 +      ce = active_cache[pos];
 +      if (ce_namelen(ce) == namelen &&
 +          !memcmp(ce->name, filename, namelen))
 +              die("Path '%s' is in the index, but not at stage %d.\n"
 +                  "Did you mean ':%d:%s'?",
 +                  filename, stage,
 +                  ce_stage(ce), filename);
 +
 +      /* Confusion between relative and absolute filenames? */
 +      fullnamelen = namelen + strlen(prefix);
 +      fullname = xmalloc(fullnamelen + 1);
 +      strcpy(fullname, prefix);
 +      strcat(fullname, filename);
 +      pos = cache_name_pos(fullname, fullnamelen);
 +      if (pos < 0)
 +              pos = -pos - 1;
 +      ce = active_cache[pos];
 +      if (ce_namelen(ce) == fullnamelen &&
 +          !memcmp(ce->name, fullname, fullnamelen))
 +              die("Path '%s' is in the index, but not '%s'.\n"
 +                  "Did you mean ':%d:%s'?",
 +                  fullname, filename,
 +                  ce_stage(ce), fullname);
 +
 +      if (!lstat(filename, &st))
 +              die("Path '%s' exists on disk, but not in the index.", filename);
 +      if (errno == ENOENT || errno == ENOTDIR)
 +              die("Path '%s' does not exist (neither on disk nor in the index).",
 +                  filename);
 +
 +      free(fullname);
 +}
 +
 +
 +int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode, int gently, const char *prefix)
  {
        int ret, bracket_depth;
        int namelen = strlen(name);
                        }
                        pos++;
                }
 +              if (!gently)
 +                      diagnose_invalid_index_path(stage, prefix, cp);
                return -1;
        }
        for (cp = name, bracket_depth = 0; *cp; cp++) {
        }
        if (*cp == ':') {
                unsigned char tree_sha1[20];
 -              if (!get_sha1_1(name, cp-name, tree_sha1))
 -                      return get_tree_entry(tree_sha1, cp+1, sha1,
 -                                            mode);
 +              char *object_name = NULL;
 +              if (!gently) {
 +                      object_name = xmalloc(cp-name+1);
 +                      strncpy(object_name, name, cp-name);
 +                      object_name[cp-name] = '\0';
 +              }
 +              if (!get_sha1_1(name, cp-name, tree_sha1)) {
 +                      const char *filename = cp+1;
 +                      ret = get_tree_entry(tree_sha1, filename, sha1, mode);
 +                      if (!gently) {
 +                              diagnose_invalid_sha1_path(prefix, filename,
 +                                                         tree_sha1, object_name);
 +                              free(object_name);
 +                      }
 +                      return ret;
 +              } else {
 +                      if (!gently)
 +                              die("Invalid object name '%s'.", object_name);
 +              }
        }
        return ret;
  }