Merge branch 'tr/rev-list-count'
authorJunio C Hamano <gitster@pobox.com>
Wed, 30 Jun 2010 18:55:38 +0000 (11:55 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 30 Jun 2010 18:55:38 +0000 (11:55 -0700)
* tr/rev-list-count:
  bash completion: Support "divergence from upstream" messages in __git_ps1
  rev-list: introduce --count option

Conflicts:
contrib/completion/git-completion.bash

1  2 
Documentation/rev-list-options.txt
contrib/completion/git-completion.bash
revision.c
revision.h

@@@ -98,6 -98,15 +98,15 @@@ you would get an output like this
  This implies the '--topo-order' option by default, but the
  '--date-order' option may also be specified.
  
+ ifdef::git-rev-list[]
+ --count::
+       Print a number stating how many commits would have been
+       listed, and suppress all other output.  When used together
+       with '--left-right', instead print the counts for left and
+       right commits, separated by a tab.
+ endif::git-rev-list[]
  ifndef::git-rev-list[]
  Diff Formatting
  ~~~~~~~~~~~~~~~
@@@ -384,14 -393,6 +393,14 @@@ Default mode:
        merges from the resulting history, as there are no selected
        commits contributing to this merge.
  
 +--ancestry-path::
 +
 +      When given a range of commits to display (e.g. 'commit1..commit2'
 +      or 'commit2 {caret}commit1'), only display commits that exist
 +      directly on the ancestry chain between the 'commit1' and
 +      'commit2', i.e. commits that are both descendants of 'commit1',
 +      and ancestors of 'commit2'.
 +
  A more detailed explanation follows.
  
  Suppose you specified `foo` as the <paths>.  We shall call commits
@@@ -448,7 -449,7 +457,7 @@@ This results in
  +
  -----------------------------------------------------------------------
          .-A---N---O
 -       /         /
 +       /     /   /
        I---------D
  -----------------------------------------------------------------------
  +
@@@ -519,6 -520,8 +528,6 @@@ Note that without '\--full-history', th
  one of the parents is TREESAME, we follow only that one, so the other
  sides of the merge are never walked.
  
 -Finally, there is a fourth simplification mode available:
 -
  --simplify-merges::
  
        First, build a history graph in the same way that
@@@ -560,46 -563,6 +569,46 @@@ Note the major differences in `N` and `
    removed completely, because it had one parent and is TREESAME.
  --
  
 +Finally, there is a fifth simplification mode available:
 +
 +--ancestry-path::
 +
 +      Limit the displayed commits to those directly on the ancestry
 +      chain between the "from" and "to" commits in the given commit
 +      range. I.e. only display commits that are ancestor of the "to"
 +      commit, and descendants of the "from" commit.
 ++
 +As an example use case, consider the following commit history:
 ++
 +-----------------------------------------------------------------------
 +          D---E-------F
 +         /     \       \
 +        B---C---G---H---I---J
 +       /                     \
 +      A-------K---------------L--M
 +-----------------------------------------------------------------------
 ++
 +A regular 'D..M' computes the set of commits that are ancestors of `M`,
 +but excludes the ones that are ancestors of `D`. This is useful to see
 +what happened to the history leading to `M` since `D`, in the sense
 +that "what does `M` have that did not exist in `D`". The result in this
 +example would be all the commits, except `A` and `B` (and `D` itself,
 +of course).
 ++
 +When we want to find out what commits in `M` are contaminated with the
 +bug introduced by `D` and need fixing, however, we might want to view
 +only the subset of 'D..M' that are actually descendants of `D`, i.e.
 +excluding `C` and `K`. This is exactly what the '\--ancestry-path'
 +option does. Applied to the 'D..M' range, it results in:
 ++
 +-----------------------------------------------------------------------
 +              E-------F
 +               \       \
 +                G---H---I---J
 +                             \
 +                              L--M
 +-----------------------------------------------------------------------
 +
  The '\--simplify-by-decoration' option allows you to view only the
  big picture of the topology of the history, by omitting commits
  that are not referenced by tags.  Commits are marked as !TREESAME
  #       set GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're
  #       untracked files, then a '%' will be shown next to the branch name.
  #
+ #       If you would like to see the difference between HEAD and its
+ #       upstream, set GIT_PS1_SHOWUPSTREAM="auto".  A "<" indicates
+ #       you are behind, ">" indicates you are ahead, and "<>"
+ #       indicates you have diverged.  You can further control
+ #       behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated
+ #       list of values:
+ #           verbose       show number of commits ahead/behind (+/-) upstream
+ #           legacy        don't use the '--count' option available in recent
+ #                         versions of git-rev-list
+ #           git           always compare HEAD to @{upstream}
+ #           svn           always compare HEAD to your SVN upstream
+ #       By default, __git_ps1 will compare HEAD to your SVN upstream
+ #       if it can find one, or @{upstream} otherwise.  Once you have
+ #       set GIT_PS1_SHOWUPSTREAM, you can override it on a
+ #       per-repository basis by setting the bash.showUpstream config
+ #       variable.
+ #
+ #
  # To submit patches:
  #
  #    *) Read Documentation/SubmittingPatches
@@@ -78,14 -96,133 +96,133 @@@ __gitdir (
        fi
  }
  
+ # stores the divergence from upstream in $p
+ # used by GIT_PS1_SHOWUPSTREAM
+ __git_ps1_show_upstream ()
+ {
+       local key value
+       local svn_remote=() svn_url_pattern count n
+       local upstream=git legacy="" verbose=""
+       # get some config options from git-config
+       while read key value; do
+               case "$key" in
+               bash.showupstream)
+                       GIT_PS1_SHOWUPSTREAM="$value"
+                       if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
+                               p=""
+                               return
+                       fi
+                       ;;
+               svn-remote.*.url)
+                       svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
+                       svn_url_pattern+="\\|$value"
+                       upstream=svn+git # default upstream is SVN if available, else git
+                       ;;
+               esac
+       done < <(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')
+       # parse configuration values
+       for option in ${GIT_PS1_SHOWUPSTREAM}; do
+               case "$option" in
+               git|svn) upstream="$option" ;;
+               verbose) verbose=1 ;;
+               legacy)  legacy=1  ;;
+               esac
+       done
+       # Find our upstream
+       case "$upstream" in
+       git)    upstream="@{upstream}" ;;
+       svn*)
+               # get the upstream from the "git-svn-id: ..." in a commit message
+               # (git-svn uses essentially the same procedure internally)
+               local svn_upstream=($(git log --first-parent -1 \
+                                       --grep="^git-svn-id: \(${svn_url_pattern:2}\)" 2>/dev/null))
+               if [[ 0 -ne ${#svn_upstream[@]} ]]; then
+                       svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
+                       svn_upstream=${svn_upstream%@*}
+                       for ((n=1; "$n" <= "${#svn_remote[@]}"; ++n)); do
+                               svn_upstream=${svn_upstream#${svn_remote[$n]}}
+                       done
+                       if [[ -z "$svn_upstream" ]]; then
+                               # default branch name for checkouts with no layout:
+                               upstream=${GIT_SVN_ID:-git-svn}
+                       else
+                               upstream=${svn_upstream#/}
+                       fi
+               elif [[ "svn+git" = "$upstream" ]]; then
+                       upstream="@{upstream}"
+               fi
+               ;;
+       esac
+       # Find how many commits we are ahead/behind our upstream
+       if [[ -z "$legacy" ]]; then
+               count="$(git rev-list --count --left-right \
+                               "$upstream"...HEAD 2>/dev/null)"
+       else
+               # produce equivalent output to --count for older versions of git
+               local commits
+               if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
+               then
+                       local commit behind=0 ahead=0
+                       for commit in $commits
+                       do
+                               case "$commit" in
+                               "<"*) let ++behind
+                                       ;;
+                               *)    let ++ahead
+                                       ;;
+                               esac
+                       done
+                       count="$behind  $ahead"
+               else
+                       count=""
+               fi
+       fi
+       # calculate the result
+       if [[ -z "$verbose" ]]; then
+               case "$count" in
+               "") # no upstream
+                       p="" ;;
+               "0      0") # equal to upstream
+                       p="=" ;;
+               "0      "*) # ahead of upstream
+                       p=">" ;;
+               *"      0") # behind upstream
+                       p="<" ;;
+               *)          # diverged from upstream
+                       p="<>" ;;
+               esac
+       else
+               case "$count" in
+               "") # no upstream
+                       p="" ;;
+               "0      0") # equal to upstream
+                       p=" u=" ;;
+               "0      "*) # ahead of upstream
+                       p=" u+${count#0 }" ;;
+               *"      0") # behind upstream
+                       p=" u-${count%  0}" ;;
+               *)          # diverged from upstream
+                       p=" u+${count#* }-${count%      *}" ;;
+               esac
+       fi
+ }
  # __git_ps1 accepts 0 or 1 arguments (i.e., format string)
  # returns text to add to bash PS1 prompt (includes branch name)
  __git_ps1 ()
  {
        local g="$(__gitdir)"
        if [ -n "$g" ]; then
 -              local r
 -              local b
 +              local r=""
 +              local b=""
                if [ -f "$g/rebase-merge/interactive" ]; then
                        r="|REBASE-i"
                        b="$(cat "$g/rebase-merge/head-name")"
                        }
                fi
  
 -              local w
 -              local i
 -              local s
 -              local u
 -              local c
 +              local w=""
 +              local i=""
 +              local s=""
 +              local u=""
 +              local c=""
+               local p=""
  
                if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
                        if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
                              u="%"
                           fi
                        fi
+                       if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
+                               __git_ps1_show_upstream
+                       fi
                fi
  
                local f="$w$i$s$u"
-               printf "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r"
+               printf "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p"
        fi
  }
  
@@@ -842,7 -984,7 +984,7 @@@ _git_checkout (
        --*)
                __gitcomp "
                        --quiet --ours --theirs --track --no-track --merge
 -                      --conflict= --patch
 +                      --conflict= --orphan --patch
                        "
                ;;
        *)
@@@ -1052,7 -1194,7 +1194,7 @@@ _git_format_patch (
                        --numbered --start-number
                        --numbered-files
                        --keep-subject
 -                      --signoff
 +                      --signoff --signature --no-signature
                        --in-reply-to= --cc=
                        --full-index --binary
                        --not --all
@@@ -1726,7 -1868,6 +1868,7 @@@ _git_config (
                format.headers
                format.numbered
                format.pretty
 +              format.signature
                format.signoff
                format.subjectprefix
                format.suffix
diff --combined revision.c
@@@ -646,93 -646,6 +646,93 @@@ static int still_interesting(struct com
        return slop-1;
  }
  
 +/*
 + * "rev-list --ancestry-path A..B" computes commits that are ancestors
 + * of B but not ancestors of A but further limits the result to those
 + * that are descendants of A.  This takes the list of bottom commits and
 + * the result of "A..B" without --ancestry-path, and limits the latter
 + * further to the ones that can reach one of the commits in "bottom".
 + */
 +static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *list)
 +{
 +      struct commit_list *p;
 +      struct commit_list *rlist = NULL;
 +      int made_progress;
 +
 +      /*
 +       * Reverse the list so that it will be likely that we would
 +       * process parents before children.
 +       */
 +      for (p = list; p; p = p->next)
 +              commit_list_insert(p->item, &rlist);
 +
 +      for (p = bottom; p; p = p->next)
 +              p->item->object.flags |= TMP_MARK;
 +
 +      /*
 +       * Mark the ones that can reach bottom commits in "list",
 +       * in a bottom-up fashion.
 +       */
 +      do {
 +              made_progress = 0;
 +              for (p = rlist; p; p = p->next) {
 +                      struct commit *c = p->item;
 +                      struct commit_list *parents;
 +                      if (c->object.flags & (TMP_MARK | UNINTERESTING))
 +                              continue;
 +                      for (parents = c->parents;
 +                           parents;
 +                           parents = parents->next) {
 +                              if (!(parents->item->object.flags & TMP_MARK))
 +                                      continue;
 +                              c->object.flags |= TMP_MARK;
 +                              made_progress = 1;
 +                              break;
 +                      }
 +              }
 +      } while (made_progress);
 +
 +      /*
 +       * NEEDSWORK: decide if we want to remove parents that are
 +       * not marked with TMP_MARK from commit->parents for commits
 +       * in the resulting list.  We may not want to do that, though.
 +       */
 +
 +      /*
 +       * The ones that are not marked with TMP_MARK are uninteresting
 +       */
 +      for (p = list; p; p = p->next) {
 +              struct commit *c = p->item;
 +              if (c->object.flags & TMP_MARK)
 +                      continue;
 +              c->object.flags |= UNINTERESTING;
 +      }
 +
 +      /* We are done with the TMP_MARK */
 +      for (p = list; p; p = p->next)
 +              p->item->object.flags &= ~TMP_MARK;
 +      for (p = bottom; p; p = p->next)
 +              p->item->object.flags &= ~TMP_MARK;
 +      free_commit_list(rlist);
 +}
 +
 +/*
 + * Before walking the history, keep the set of "negative" refs the
 + * caller has asked to exclude.
 + *
 + * This is used to compute "rev-list --ancestry-path A..B", as we need
 + * to filter the result of "A..B" further to the ones that can actually
 + * reach A.
 + */
 +static struct commit_list *collect_bottom_commits(struct commit_list *list)
 +{
 +      struct commit_list *elem, *bottom = NULL;
 +      for (elem = list; elem; elem = elem->next)
 +              if (elem->item->object.flags & UNINTERESTING)
 +                      commit_list_insert(elem->item, &bottom);
 +      return bottom;
 +}
 +
  static int limit_list(struct rev_info *revs)
  {
        int slop = SLOP;
        struct commit_list *list = revs->commits;
        struct commit_list *newlist = NULL;
        struct commit_list **p = &newlist;
 +      struct commit_list *bottom = NULL;
 +
 +      if (revs->ancestry_path) {
 +              bottom = collect_bottom_commits(list);
 +              if (!bottom)
 +                      die("--ancestry-path given but there are no bottom commits");
 +      }
  
        while (list) {
                struct commit_list *entry = list;
        if (revs->cherry_pick)
                cherry_pick_list(newlist, revs);
  
 +      if (bottom) {
 +              limit_to_ancestry(bottom, newlist);
 +              free_commit_list(bottom);
 +      }
 +
        revs->commits = newlist;
        return 0;
  }
@@@ -1162,22 -1063,18 +1162,22 @@@ static int handle_revision_opt(struct r
  
        if (!prefixcmp(arg, "--max-count=")) {
                revs->max_count = atoi(arg + 12);
 +              revs->no_walk = 0;
        } else if (!prefixcmp(arg, "--skip=")) {
                revs->skip_count = atoi(arg + 7);
        } else if ((*arg == '-') && isdigit(arg[1])) {
        /* accept -<digit>, like traditional "head" */
                revs->max_count = atoi(arg + 1);
 +              revs->no_walk = 0;
        } else if (!strcmp(arg, "-n")) {
                if (argc <= 1)
                        return error("-n requires an argument");
                revs->max_count = atoi(argv[1]);
 +              revs->no_walk = 0;
                return 2;
        } else if (!prefixcmp(arg, "-n")) {
                revs->max_count = atoi(arg + 2);
 +              revs->no_walk = 0;
        } else if (!prefixcmp(arg, "--max-age=")) {
                revs->max_age = atoi(arg + 10);
        } else if (!prefixcmp(arg, "--since=")) {
                revs->min_age = approxidate(arg + 8);
        } else if (!strcmp(arg, "--first-parent")) {
                revs->first_parent_only = 1;
 +      } else if (!strcmp(arg, "--ancestry-path")) {
 +              revs->ancestry_path = 1;
 +              revs->simplify_history = 0;
 +              revs->limited = 1;
        } else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
                init_reflog_walk(&revs->reflog_info);
        } else if (!strcmp(arg, "--default")) {
                revs->boundary = 1;
        } else if (!strcmp(arg, "--left-right")) {
                revs->left_right = 1;
+       } else if (!strcmp(arg, "--count")) {
+               revs->count = 1;
        } else if (!strcmp(arg, "--cherry-pick")) {
                revs->cherry_pick = 1;
                revs->limited = 1;
@@@ -1888,7 -1783,7 +1890,7 @@@ int prepare_revision_walk(struct rev_in
  enum rewrite_result {
        rewrite_one_ok,
        rewrite_one_noparents,
 -      rewrite_one_error,
 +      rewrite_one_error
  };
  
  static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
diff --combined revision.h
@@@ -57,6 -57,7 +57,7 @@@ struct rev_info 
                        limited:1,
                        unpacked:1,
                        boundary:2,
+                       count:1,
                        left_right:1,
                        rewrite_parents:1,
                        print_parents:1,
@@@ -66,7 -67,6 +67,7 @@@
                        reverse_output_stage:1,
                        cherry_pick:1,
                        bisect:1,
 +                      ancestry_path:1,
                        first_parent_only:1;
  
        /* Diff flags */
  
        /* notes-specific options: which refs to show */
        struct display_notes_opt notes_opt;
+       /* commit counts */
+       int count_left;
+       int count_right;
  };
  
  #define REV_TREE_SAME         0