Merge branch 'ab/ref-filter-no-contains'
authorJunio C Hamano <gitster@pobox.com>
Tue, 11 Apr 2017 07:21:50 +0000 (00:21 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 11 Apr 2017 07:21:50 +0000 (00:21 -0700)
"git tag/branch/for-each-ref" family of commands long allowed to
filter the refs by "--contains X" (show only the refs that are
descendants of X), "--merged X" (show only the refs that are
ancestors of X), "--no-merged X" (show only the refs that are not
ancestors of X). One curious omission, "--no-contains X" (show
only the refs that are not descendants of X) has been added to
them.

* ab/ref-filter-no-contains:
tag: add tests for --with and --without
ref-filter: reflow recently changed branch/tag/for-each-ref docs
ref-filter: add --no-contains option to tag/branch/for-each-ref
tag: change --point-at to default to HEAD
tag: implicitly supply --list given another list-like option
tag: change misleading --list <pattern> documentation
parse-options: add OPT_NONEG to the "contains" option
tag: add more incompatibles mode tests
for-each-ref: partly change <object> to <commit> in help
tag tests: fix a typo in a test description
tag: remove a TODO item from the test suite
ref-filter: add test for --contains on a non-commit
ref-filter: make combining --merged & --no-merged an error
tag doc: reword --[no-]merged to talk about commits, not tips
tag doc: split up the --[no-]merged documentation
tag doc: move the description of --[no-]merged earlier

1  2 
Documentation/git-branch.txt
builtin/branch.c
contrib/completion/git-completion.bash
ref-filter.c
t/t3200-branch.sh
t/t7004-tag.sh
index c203e8c07352f0f5efcb47bab376c4e03ad44e24,5e175ec3399b54dc36fa10c3daba416855935a7d..81bd0a7b7741f175cf7a99e2aa9cbcacf42da78e
@@@ -10,8 -10,9 +10,9 @@@ SYNOPSI
  [verse]
  'git branch' [--color[=<when>] | --no-color] [-r | -a]
        [--list] [-v [--abbrev=<length> | --no-abbrev]]
-       [--column[=<options>] | --no-column]
-       [(--merged | --no-merged | --contains) [<commit>]] [--sort=<key>]
+       [--column[=<options>] | --no-column] [--sort=<key>]
+       [(--merged | --no-merged) [<commit>]]
+       [--contains [<commit]] [--no-contains [<commit>]]
        [--points-at <object>] [--format=<format>] [<pattern>...]
  'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>]
  'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
@@@ -35,11 -36,12 +36,12 @@@ as branch creation
  
  With `--contains`, shows only the branches that contain the named commit
  (in other words, the branches whose tip commits are descendants of the
- named commit).  With `--merged`, only branches merged into the named
- commit (i.e. the branches whose tip commits are reachable from the named
- commit) will be listed.  With `--no-merged` only branches not merged into
- the named commit will be listed.  If the <commit> argument is missing it
- defaults to `HEAD` (i.e. the tip of the current branch).
+ named commit), `--no-contains` inverts it. With `--merged`, only branches
+ merged into the named commit (i.e. the branches whose tip commits are
+ reachable from the named commit) will be listed.  With `--no-merged` only
+ branches not merged into the named commit will be listed.  If the <commit>
+ argument is missing it defaults to `HEAD` (i.e. the tip of the current
+ branch).
  
  The command's second form creates a new branch head named <branchname>
  which points to the current `HEAD`, or <start-point> if given.
@@@ -142,13 -144,8 +144,13 @@@ This option is only applicable in non-v
        List both remote-tracking branches and local branches.
  
  --list::
 -      Activate the list mode. `git branch <pattern>` would try to create a branch,
 -      use `git branch --list <pattern>` to list matching branches.
 +      List branches.  With optional `<pattern>...`, e.g. `git
 +      branch --list 'maint-*'`, list only the branches that match
 +      the pattern(s).
 ++
 +This should not be confused with `git branch -l <branchname>`,
 +which creates a branch named `<branchname>` with a reflog.
 +See `--create-reflog` above for details.
  
  -v::
  -vv::
@@@ -218,13 -215,19 +220,19 @@@ start-point is either a local or remote
        Only list branches which contain the specified commit (HEAD
        if not specified). Implies `--list`.
  
+ --no-contains [<commit>]::
+       Only list branches which don't contain the specified commit
+       (HEAD if not specified). Implies `--list`.
  --merged [<commit>]::
        Only list branches whose tips are reachable from the
-       specified commit (HEAD if not specified). Implies `--list`.
+       specified commit (HEAD if not specified). Implies `--list`,
+       incompatible with `--no-merged`.
  
  --no-merged [<commit>]::
        Only list branches whose tips are not reachable from the
-       specified commit (HEAD if not specified). Implies `--list`.
+       specified commit (HEAD if not specified). Implies `--list`,
+       incompatible with `--merged`.
  
  <branchname>::
        The name of the branch to create or delete.
@@@ -301,13 -304,16 +309,16 @@@ If you are creating a branch that you w
  easier to use the git checkout command with its `-b` option to create
  a branch and check it out with a single command.
  
- The options `--contains`, `--merged` and `--no-merged` serve three related
- but different purposes:
+ The options `--contains`, `--no-contains`, `--merged` and `--no-merged`
serve four related but different purposes:
  
  - `--contains <commit>` is used to find all branches which will need
    special attention if <commit> were to be rebased or amended, since those
    branches contain the specified <commit>.
  
+ - `--no-contains <commit>` is the inverse of that, i.e. branches that don't
+   contain the specified <commit>.
  - `--merged` is used to find all branches which can be safely deleted,
    since those branches are fully contained by HEAD.
  
diff --combined builtin/branch.c
index 52688f2e1be91024ba6cb170b4b38b393e8ab8ba,5e7c8d4665e61b9a941fb63c4903f65288e7b2ad..0552c42ad115bba218f35d4836f1021e3385f6c2
@@@ -33,7 -33,7 +33,7 @@@ static const char * const builtin_branc
  };
  
  static const char *head;
 -static unsigned char head_sha1[20];
 +static struct object_id head_oid;
  
  static int branch_use_color = -1;
  static char branch_colors[][COLOR_MAXLEN] = {
@@@ -118,13 -118,13 +118,13 @@@ static int branch_merged(int kind, cons
        if (kind == FILTER_REFS_BRANCHES) {
                struct branch *branch = branch_get(name);
                const char *upstream = branch_get_upstream(branch, NULL);
 -              unsigned char sha1[20];
 +              struct object_id oid;
  
                if (upstream &&
                    (reference_name = reference_name_to_free =
                     resolve_refdup(upstream, RESOLVE_REF_READING,
 -                                  sha1, NULL)) != NULL)
 -                      reference_rev = lookup_commit_reference(sha1);
 +                                  oid.hash, NULL)) != NULL)
 +                      reference_rev = lookup_commit_reference(oid.hash);
        }
        if (!reference_rev)
                reference_rev = head_rev;
  }
  
  static int check_branch_commit(const char *branchname, const char *refname,
 -                             const unsigned char *sha1, struct commit *head_rev,
 +                             const struct object_id *oid, struct commit *head_rev,
                               int kinds, int force)
  {
 -      struct commit *rev = lookup_commit_reference(sha1);
 +      struct commit *rev = lookup_commit_reference(oid->hash);
        if (!rev) {
                error(_("Couldn't look up commit object for '%s'"), refname);
                return -1;
@@@ -184,34 -184,31 +184,34 @@@ static int delete_branches(int argc, co
                           int quiet)
  {
        struct commit *head_rev = NULL;
 -      unsigned char sha1[20];
 +      struct object_id oid;
        char *name = NULL;
        const char *fmt;
        int i;
        int ret = 0;
        int remote_branch = 0;
        struct strbuf bname = STRBUF_INIT;
 +      unsigned allowed_interpret;
  
        switch (kinds) {
        case FILTER_REFS_REMOTES:
                fmt = "refs/remotes/%s";
                /* For subsequent UI messages */
                remote_branch = 1;
 +              allowed_interpret = INTERPRET_BRANCH_REMOTE;
  
                force = 1;
                break;
        case FILTER_REFS_BRANCHES:
                fmt = "refs/heads/%s";
 +              allowed_interpret = INTERPRET_BRANCH_LOCAL;
                break;
        default:
                die(_("cannot use -a with -d"));
        }
  
        if (!force) {
 -              head_rev = lookup_commit_reference(head_sha1);
 +              head_rev = lookup_commit_reference(head_oid.hash);
                if (!head_rev)
                        die(_("Couldn't look up commit object for HEAD"));
        }
                char *target = NULL;
                int flags = 0;
  
 -              strbuf_branchname(&bname, argv[i]);
 +              strbuf_branchname(&bname, argv[i], allowed_interpret);
                free(name);
                name = mkpathdup(fmt, bname.buf);
  
                                        RESOLVE_REF_READING
                                        | RESOLVE_REF_NO_RECURSE
                                        | RESOLVE_REF_ALLOW_BAD_NAME,
 -                                      sha1, &flags);
 +                                      oid.hash, &flags);
                if (!target) {
                        error(remote_branch
                              ? _("remote-tracking branch '%s' not found.")
                }
  
                if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
 -                  check_branch_commit(bname.buf, name, sha1, head_rev, kinds,
 +                  check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
                                        force)) {
                        ret = 1;
                        goto next;
                }
  
 -              if (delete_ref(NULL, name, is_null_sha1(sha1) ? NULL : sha1,
 +              if (delete_ref(NULL, name, is_null_oid(&oid) ? NULL : oid.hash,
                               REF_NODEREF)) {
                        error(remote_branch
                              ? _("Error deleting remote-tracking branch '%s'")
                               bname.buf,
                               (flags & REF_ISBROKEN) ? "broken"
                               : (flags & REF_ISSYMREF) ? target
 -                             : find_unique_abbrev(sha1, DEFAULT_ABBREV));
 +                             : find_unique_abbrev(oid.hash, DEFAULT_ABBREV));
                }
                delete_branch_config(bname.buf);
  
@@@ -338,18 -335,9 +338,18 @@@ static char *build_format(struct ref_fi
                    branch_get_color(BRANCH_COLOR_CURRENT));
  
        if (filter->verbose) {
 +              struct strbuf obname = STRBUF_INIT;
 +
 +              if (filter->abbrev < 0)
 +                      strbuf_addf(&obname, "%%(objectname:short)");
 +              else if (!filter->abbrev)
 +                      strbuf_addf(&obname, "%%(objectname)");
 +              else
 +                      strbuf_addf(&obname, "%%(objectname:short=%d)", filter->abbrev);
 +
                strbuf_addf(&local, "%%(align:%d,left)%%(refname:lstrip=2)%%(end)", maxwidth);
                strbuf_addf(&local, "%s", branch_get_color(BRANCH_COLOR_RESET));
 -              strbuf_addf(&local, " %%(objectname:short=7) ");
 +              strbuf_addf(&local, " %s ", obname.buf);
  
                if (filter->verbose > 1)
                        strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
                else
                        strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)");
  
 -              strbuf_addf(&remote, "%s%%(align:%d,left)%s%%(refname:lstrip=2)%%(end)%s%%(if)%%(symref)%%(then) -> %%(symref:short)"
 -                          "%%(else) %%(objectname:short=7) %%(contents:subject)%%(end)",
 +              strbuf_addf(&remote, "%s%%(align:%d,left)%s%%(refname:lstrip=2)%%(end)%s"
 +                          "%%(if)%%(symref)%%(then) -> %%(symref:short)"
 +                          "%%(else) %s %%(contents:subject)%%(end)",
                            branch_get_color(BRANCH_COLOR_REMOTE), maxwidth, quote_literal_for_format(remote_prefix),
 -                          branch_get_color(BRANCH_COLOR_RESET));
 +                          branch_get_color(BRANCH_COLOR_RESET), obname.buf);
 +              strbuf_release(&obname);
        } else {
                strbuf_addf(&local, "%%(refname:lstrip=2)%s%%(if)%%(symref)%%(then) -> %%(symref:short)%%(end)",
                            branch_get_color(BRANCH_COLOR_RESET));
@@@ -562,7 -548,9 +562,9 @@@ int cmd_branch(int argc, const char **a
                OPT_SET_INT('r', "remotes",     &filter.kind, N_("act on remote-tracking branches"),
                        FILTER_REFS_REMOTES),
                OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")),
+               OPT_NO_CONTAINS(&filter.no_commit, N_("print only branches that don't contain the commit")),
                OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")),
+               OPT_WITHOUT(&filter.no_commit, N_("print only branches that don't contain the commit")),
                OPT__ABBREV(&filter.abbrev),
  
                OPT_GROUP(N_("Specific git-branch actions:")),
  
        track = git_branch_track;
  
 -      head = resolve_refdup("HEAD", 0, head_sha1, NULL);
 +      head = resolve_refdup("HEAD", 0, head_oid.hash, NULL);
        if (!head)
                die(_("Failed to resolve HEAD as a valid ref."));
        if (!strcmp(head, "HEAD"))
        if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
                list = 1;
  
-       if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr)
+       if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
+           filter.no_commit)
                list = 1;
  
        if (!!delete + !!rename + !!new_upstream +
index dd862e018dc29e878191cef7c6a547588f1befbb,ec8fce5820f4eb08fea444740f4b793006afb4e7..1150164d5ce0e7396191a192189b43444883f739
@@@ -213,20 -213,6 +213,20 @@@ _get_comp_words_by_ref (
  }
  fi
  
 +# Fills the COMPREPLY array with prefiltered words without any additional
 +# processing.
 +# Callers must take care of providing only words that match the current word
 +# to be completed and adding any prefix and/or suffix (trailing space!), if
 +# necessary.
 +# 1: List of newline-separated matching completion words, complete with
 +#    prefix and suffix.
 +__gitcomp_direct ()
 +{
 +      local IFS=$'\n'
 +
 +      COMPREPLY=($1)
 +}
 +
  __gitcompappend ()
  {
        local x i=${#COMPREPLY[@]}
@@@ -352,27 -338,14 +352,27 @@@ __git_index_files (
        done | sort | uniq
  }
  
 +# Lists branches from the local repository.
 +# 1: A prefix to be added to each listed branch (optional).
 +# 2: List only branches matching this word (optional; list all branches if
 +#    unset or empty).
 +# 3: A suffix to be appended to each listed branch (optional).
  __git_heads ()
  {
 -      __git for-each-ref --format='%(refname:short)' refs/heads
 +      local pfx="${1-}" cur_="${2-}" sfx="${3-}"
 +
 +      __git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
 +                      "refs/heads/$cur_*" "refs/heads/$cur_*/**"
  }
  
 +# Lists tags from the local repository.
 +# Accepts the same positional parameters as __git_heads() above.
  __git_tags ()
  {
 -      __git for-each-ref --format='%(refname:short)' refs/tags
 +      local pfx="${1-}" cur_="${2-}" sfx="${3-}"
 +
 +      __git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
 +                      "refs/tags/$cur_*" "refs/tags/$cur_*/**"
  }
  
  # Lists refs from the local (by default) or from a remote repository.
  #    Can be the name of a configured remote, a path, or a URL.
  # 2: In addition to local refs, list unique branches from refs/remotes/ for
  #    'git checkout's tracking DWIMery (optional; ignored, if set but empty).
 +# 3: A prefix to be added to each listed ref (optional).
 +# 4: List only refs matching this word (optional; list all refs if unset or
 +#    empty).
 +# 5: A suffix to be appended to each listed ref (optional; ignored, if set
 +#    but empty).
 +#
 +# Use __git_complete_refs() instead.
  __git_refs ()
  {
        local i hash dir track="${2-}"
        local list_refs_from=path remote="${1-}"
 -      local format refs pfx
 +      local format refs
 +      local pfx="${3-}" cur_="${4-$cur}" sfx="${5-}"
 +      local match="${4-}"
 +      local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
  
        __git_find_repo_path
        dir="$__git_repo_path"
        fi
  
        if [ "$list_refs_from" = path ]; then
 -              case "$cur" in
 +              if [[ "$cur_" == ^* ]]; then
 +                      pfx="$pfx^"
 +                      fer_pfx="$fer_pfx^"
 +                      cur_=${cur_#^}
 +                      match=${match#^}
 +              fi
 +              case "$cur_" in
                refs|refs/*)
                        format="refname"
 -                      refs="${cur%/*}"
 +                      refs=("$match*" "$match*/**")
                        track=""
                        ;;
                *)
 -                      [[ "$cur" == ^* ]] && pfx="^"
                        for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do
 -                              if [ -e "$dir/$i" ]; then echo $pfx$i; fi
 +                              case "$i" in
 +                              $match*)
 +                                      if [ -e "$dir/$i" ]; then
 +                                              echo "$pfx$i$sfx"
 +                                      fi
 +                                      ;;
 +                              esac
                        done
 -                      format="refname:short"
 -                      refs="refs/tags refs/heads refs/remotes"
 +                      format="refname:strip=2"
 +                      refs=("refs/tags/$match*" "refs/tags/$match*/**"
 +                              "refs/heads/$match*" "refs/heads/$match*/**"
 +                              "refs/remotes/$match*" "refs/remotes/$match*/**")
                        ;;
                esac
 -              __git_dir="$dir" __git for-each-ref --format="$pfx%($format)" \
 -                      $refs
 +              __git_dir="$dir" __git for-each-ref --format="$fer_pfx%($format)$sfx" \
 +                      "${refs[@]}"
                if [ -n "$track" ]; then
                        # employ the heuristic used by git checkout
                        # Try to find a remote branch that matches the completion word
                        # but only output if the branch name is unique
 -                      local ref entry
 -                      __git for-each-ref --shell --format="ref=%(refname:short)" \
 -                              "refs/remotes/" | \
 -                      while read -r entry; do
 -                              eval "$entry"
 -                              ref="${ref#*/}"
 -                              if [[ "$ref" == "$cur"* ]]; then
 -                                      echo "$ref"
 -                              fi
 -                      done | sort | uniq -u
 +                      __git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
 +                              --sort="refname:strip=3" \
 +                              "refs/remotes/*/$match*" "refs/remotes/*/$match*/**" | \
 +                      uniq -u
                fi
                return
        fi
 -      case "$cur" in
 +      case "$cur_" in
        refs|refs/*)
 -              __git ls-remote "$remote" "$cur*" | \
 +              __git ls-remote "$remote" "$match*" | \
                while read -r hash i; do
                        case "$i" in
                        *^{}) ;;
 -                      *) echo "$i" ;;
 +                      *) echo "$pfx$i$sfx" ;;
                        esac
                done
                ;;
        *)
                if [ "$list_refs_from" = remote ]; then
 -                      echo "HEAD"
 -                      __git for-each-ref --format="%(refname:short)" \
 -                              "refs/remotes/$remote/" | sed -e "s#^$remote/##"
 +                      case "HEAD" in
 +                      $match*)        echo "${pfx}HEAD$sfx" ;;
 +                      esac
 +                      __git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
 +                              "refs/remotes/$remote/$match*" \
 +                              "refs/remotes/$remote/$match*/**"
                else
 -                      __git ls-remote "$remote" HEAD \
 -                              "refs/tags/*" "refs/heads/*" "refs/remotes/*" |
 +                      local query_symref
 +                      case "HEAD" in
 +                      $match*)        query_symref="HEAD" ;;
 +                      esac
 +                      __git ls-remote "$remote" $query_symref \
 +                              "refs/tags/$match*" "refs/heads/$match*" \
 +                              "refs/remotes/$match*" |
                        while read -r hash i; do
                                case "$i" in
                                *^{})   ;;
 -                              refs/*) echo "${i#refs/*/}" ;;
 -                              *)      echo "$i" ;;  # symbolic refs
 +                              refs/*) echo "$pfx${i#refs/*/}$sfx" ;;
 +                              *)      echo "$pfx$i$sfx" ;;  # symbolic refs
                                esac
                        done
                fi
        esac
  }
  
 +# Completes refs, short and long, local and remote, symbolic and pseudo.
 +#
 +# Usage: __git_complete_refs [<option>]...
 +# --remote=<remote>: The remote to list refs from, can be the name of a
 +#                    configured remote, a path, or a URL.
 +# --track: List unique remote branches for 'git checkout's tracking DWIMery.
 +# --pfx=<prefix>: A prefix to be added to each ref.
 +# --cur=<word>: The current ref to be completed.  Defaults to the current
 +#               word to be completed.
 +# --sfx=<suffix>: A suffix to be appended to each ref instead of the default
 +#                 space.
 +__git_complete_refs ()
 +{
 +      local remote track pfx cur_="$cur" sfx=" "
 +
 +      while test $# != 0; do
 +              case "$1" in
 +              --remote=*)     remote="${1##--remote=}" ;;
 +              --track)        track="yes" ;;
 +              --pfx=*)        pfx="${1##--pfx=}" ;;
 +              --cur=*)        cur_="${1##--cur=}" ;;
 +              --sfx=*)        sfx="${1##--sfx=}" ;;
 +              *)              return 1 ;;
 +              esac
 +              shift
 +      done
 +
 +      __gitcomp_direct "$(__git_refs "$remote" "$track" "$pfx" "$cur_" "$sfx")"
 +}
 +
  # __git_refs2 requires 1 argument (to pass to __git_refs)
 +# Deprecated: use __git_complete_fetch_refspecs() instead.
  __git_refs2 ()
  {
        local i
        done
  }
  
 +# Completes refspecs for fetching from a remote repository.
 +# 1: The remote repository.
 +# 2: A prefix to be added to each listed refspec (optional).
 +# 3: The ref to be completed as a refspec instead of the current word to be
 +#    completed (optional)
 +# 4: A suffix to be appended to each listed refspec instead of the default
 +#    space (optional).
 +__git_complete_fetch_refspecs ()
 +{
 +      local i remote="$1" pfx="${2-}" cur_="${3-$cur}" sfx="${4- }"
 +
 +      __gitcomp_direct "$(
 +              for i in $(__git_refs "$remote" "" "" "$cur_") ; do
 +                      echo "$pfx$i:$i$sfx"
 +              done
 +              )"
 +}
 +
  # __git_refs_remotes requires 1 argument (to pass to ls-remote)
  __git_refs_remotes ()
  {
@@@ -655,15 -554,15 +655,15 @@@ __git_complete_revlist_file (
        *...*)
                pfx="${cur_%...*}..."
                cur_="${cur_#*...}"
 -              __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
 +              __git_complete_refs --pfx="$pfx" --cur="$cur_"
                ;;
        *..*)
                pfx="${cur_%..*}.."
                cur_="${cur_#*..}"
 -              __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
 +              __git_complete_refs --pfx="$pfx" --cur="$cur_"
                ;;
        *)
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
                ;;
        esac
  }
@@@ -748,23 -647,23 +748,23 @@@ __git_complete_remote_or_refspec (
        case "$cmd" in
        fetch)
                if [ $lhs = 1 ]; then
 -                      __gitcomp_nl "$(__git_refs2 "$remote")" "$pfx" "$cur_"
 +                      __git_complete_fetch_refspecs "$remote" "$pfx" "$cur_"
                else
 -                      __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
 +                      __git_complete_refs --pfx="$pfx" --cur="$cur_"
                fi
                ;;
        pull|remote)
                if [ $lhs = 1 ]; then
 -                      __gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_"
 +                      __git_complete_refs --remote="$remote" --pfx="$pfx" --cur="$cur_"
                else
 -                      __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
 +                      __git_complete_refs --pfx="$pfx" --cur="$cur_"
                fi
                ;;
        push)
                if [ $lhs = 1 ]; then
 -                      __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
 +                      __git_complete_refs --pfx="$pfx" --cur="$cur_"
                else
 -                      __gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_"
 +                      __git_complete_refs --remote="$remote" --pfx="$pfx" --cur="$cur_"
                fi
                ;;
        esac
@@@ -1167,7 -1066,7 +1167,7 @@@ _git_bisect (
  
        case "$subcommand" in
        bad|good|reset|skip|start)
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
                ;;
        *)
                ;;
@@@ -1189,12 -1088,12 +1189,12 @@@ _git_branch (
  
        case "$cur" in
        --set-upstream-to=*)
 -              __gitcomp_nl "$(__git_refs)" "" "${cur##--set-upstream-to=}"
 +              __git_complete_refs --cur="${cur##--set-upstream-to=}"
                ;;
        --*)
                __gitcomp "
                        --color --no-color --verbose --abbrev= --no-abbrev
-                       --track --no-track --contains --merged --no-merged
+                       --track --no-track --contains --no-contains --merged --no-merged
                        --set-upstream-to= --edit-description --list
                        --unset-upstream --delete --move --remotes
                        --column --no-column --sort= --points-at
                ;;
        *)
                if [ $only_local_ref = "y" -a $has_r = "n" ]; then
 -                      __gitcomp_nl "$(__git_heads)"
 +                      __gitcomp_direct "$(__git_heads "" "$cur" " ")"
                else
 -                      __gitcomp_nl "$(__git_refs)"
 +                      __git_complete_refs
                fi
                ;;
        esac
@@@ -1247,18 -1146,18 +1247,18 @@@ _git_checkout (
        *)
                # check if --track, --no-track, or --no-guess was specified
                # if so, disable DWIM mode
 -              local flags="--track --no-track --no-guess" track=1
 +              local flags="--track --no-track --no-guess" track_opt="--track"
                if [ -n "$(__git_find_on_cmdline "$flags")" ]; then
 -                      track=''
 +                      track_opt=''
                fi
 -              __gitcomp_nl "$(__git_refs '' $track)"
 +              __git_complete_refs $track_opt
                ;;
        esac
  }
  
  _git_cherry ()
  {
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_cherry_pick ()
                __gitcomp "--edit --no-commit --signoff --strategy= --mainline"
                ;;
        *)
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
                ;;
        esac
  }
@@@ -1325,7 -1224,7 +1325,7 @@@ _git_commit (
  {
        case "$prev" in
        -c|-C)
 -              __gitcomp_nl "$(__git_refs)" "" "${cur}"
 +              __git_complete_refs
                return
                ;;
        esac
                ;;
        --reuse-message=*|--reedit-message=*|\
        --fixup=*|--squash=*)
 -              __gitcomp_nl "$(__git_refs)" "" "${cur#*=}"
 +              __git_complete_refs --cur="${cur#*=}"
                return
                ;;
        --untracked-files=*)
@@@ -1378,7 -1277,7 +1378,7 @@@ _git_describe (
                        "
                return
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  __git_diff_algorithms="myers minimal patience histogram"
@@@ -1529,43 -1428,8 +1529,43 @@@ _git_gitk (
        _gitk
  }
  
 -__git_match_ctag() {
 -      awk "/^${1//\//\\/}/ { print \$1 }" "$2"
 +# Lists matching symbol names from a tag (as in ctags) file.
 +# 1: List symbol names matching this word.
 +# 2: The tag file to list symbol names from.
 +# 3: A prefix to be added to each listed symbol name (optional).
 +# 4: A suffix to be appended to each listed symbol name (optional).
 +__git_match_ctag () {
 +      awk -v pfx="${3-}" -v sfx="${4-}" "
 +              /^${1//\//\\/}/ { print pfx \$1 sfx }
 +              " "$2"
 +}
 +
 +# Complete symbol names from a tag file.
 +# Usage: __git_complete_symbol [<option>]...
 +# --tags=<file>: The tag file to list symbol names from instead of the
 +#                default "tags".
 +# --pfx=<prefix>: A prefix to be added to each symbol name.
 +# --cur=<word>: The current symbol name to be completed.  Defaults to
 +#               the current word to be completed.
 +# --sfx=<suffix>: A suffix to be appended to each symbol name instead
 +#                 of the default space.
 +__git_complete_symbol () {
 +      local tags=tags pfx="" cur_="${cur-}" sfx=" "
 +
 +      while test $# != 0; do
 +              case "$1" in
 +              --tags=*)       tags="${1##--tags=}" ;;
 +              --pfx=*)        pfx="${1##--pfx=}" ;;
 +              --cur=*)        cur_="${1##--cur=}" ;;
 +              --sfx=*)        sfx="${1##--sfx=}" ;;
 +              *)              return 1 ;;
 +              esac
 +              shift
 +      done
 +
 +      if test -r "$tags"; then
 +              __gitcomp_direct "$(__git_match_ctag "$cur_" "$tags" "$pfx" "$sfx")"
 +      fi
  }
  
  _git_grep ()
  
        case "$cword,$prev" in
        2,*|*,-*)
 -              if test -r tags; then
 -                      __gitcomp_nl "$(__git_match_ctag "$cur" tags)"
 -                      return
 -              fi
 +              __git_complete_symbol && return
                ;;
        esac
  
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_help ()
@@@ -1706,19 -1573,6 +1706,19 @@@ _git_log (
        if [ -f "$__git_repo_path/MERGE_HEAD" ]; then
                merge="--merge"
        fi
 +      case "$prev,$cur" in
 +      -L,:*:*)
 +              return  # fall back to Bash filename completion
 +              ;;
 +      -L,:*)
 +              __git_complete_symbol --cur="${cur#:}" --sfx=":"
 +              return
 +              ;;
 +      -G,*|-S,*)
 +              __git_complete_symbol
 +              return
 +              ;;
 +      esac
        case "$cur" in
        --pretty=*|--format=*)
                __gitcomp "$__git_log_pretty_formats $(__git_pretty_aliases)
                        "
                return
                ;;
 +      -L:*:*)
 +              return  # fall back to Bash filename completion
 +              ;;
 +      -L:*)
 +              __git_complete_symbol --cur="${cur#-L:}" --sfx=":"
 +              return
 +              ;;
 +      -G*)
 +              __git_complete_symbol --pfx="-G" --cur="${cur#-G}"
 +              return
 +              ;;
 +      -S*)
 +              __git_complete_symbol --pfx="-S" --cur="${cur#-S}"
 +              return
 +              ;;
        esac
        __git_complete_revlist
  }
@@@ -1801,7 -1640,7 +1801,7 @@@ _git_merge (
                        --rerere-autoupdate --no-rerere-autoupdate --abort --continue"
                return
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_mergetool ()
@@@ -1826,7 -1665,7 +1826,7 @@@ _git_merge_base (
                return
                ;;
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_mv ()
@@@ -1864,7 -1703,7 +1864,7 @@@ _git_notes (
        ,*)
                case "$prev" in
                --ref)
 -                      __gitcomp_nl "$(__git_refs)"
 +                      __git_complete_refs
                        ;;
                *)
                        __gitcomp "$subcommands --ref"
                ;;
        add,--reuse-message=*|append,--reuse-message=*|\
        add,--reedit-message=*|append,--reedit-message=*)
 -              __gitcomp_nl "$(__git_refs)" "" "${cur#*=}"
 +              __git_complete_refs --cur="${cur#*=}"
                ;;
        add,--*|append,--*)
                __gitcomp '--file= --message= --reedit-message=
                -m|-F)
                        ;;
                *)
 -                      __gitcomp_nl "$(__git_refs)"
 +                      __git_complete_refs
                        ;;
                esac
                ;;
@@@ -1930,10 -1769,10 +1930,10 @@@ __git_complete_force_with_lease (
        --*=)
                ;;
        *:*)
 -              __gitcomp_nl "$(__git_refs)" "" "${cur_#*:}"
 +              __git_complete_refs --cur="${cur_#*:}"
                ;;
        *)
 -              __gitcomp_nl "$(__git_refs)" "" "$cur_"
 +              __git_complete_refs --cur="$cur_"
                ;;
        esac
  }
@@@ -2009,7 -1848,7 +2009,7 @@@ _git_rebase (
  
                return
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_reflog ()
        if [ -z "$subcommand" ]; then
                __gitcomp "$subcommands"
        else
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
        fi
  }
  
@@@ -2166,7 -2005,7 +2166,7 @@@ _git_config (
                return
                ;;
        branch.*.merge)
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
                return
                ;;
        branch.*.rebase)
                ;;
        branch.*)
                local pfx="${cur%.*}." cur_="${cur#*.}"
 -              __gitcomp_nl "$(__git_heads)" "$pfx" "$cur_" "."
 +              __gitcomp_direct "$(__git_heads "$pfx" "$cur_" ".")"
                __gitcomp_nl_append $'autosetupmerge\nautosetuprebase\n' "$pfx" "$cur_"
                return
                ;;
@@@ -2677,7 -2516,7 +2677,7 @@@ _git_replace (
                return
                ;;
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_rerere ()
@@@ -2701,7 -2540,7 +2701,7 @@@ _git_reset (
                return
                ;;
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_revert ()
                return
                ;;
        esac
 -      __gitcomp_nl "$(__git_refs)"
 +      __git_complete_refs
  }
  
  _git_rm ()
@@@ -2828,7 -2667,7 +2828,7 @@@ _git_stash (
                        ;;
                branch,*)
                        if [ $cword -eq 3 ]; then
 -                              __gitcomp_nl "$(__git_refs)";
 +                              __git_complete_refs
                        else
                                __gitcomp_nl "$(__git stash list \
                                                | sed -n -e 's/:.*//p')"
@@@ -2995,7 -2834,7 +2995,7 @@@ _git_tag (
                i="${words[c]}"
                case "$i" in
                -d|-v)
 -                      __gitcomp_nl "$(__git_tags)"
 +                      __gitcomp_direct "$(__git_tags "" "$cur" " ")"
                        return
                        ;;
                -f)
                ;;
        -*|tag)
                if [ $f = 1 ]; then
 -                      __gitcomp_nl "$(__git_tags)"
 +                      __gitcomp_direct "$(__git_tags "" "$cur" " ")"
                fi
                ;;
        *)
 -              __gitcomp_nl "$(__git_refs)"
 +              __git_complete_refs
                ;;
        esac
  
                __gitcomp "
                        --list --delete --verify --annotate --message --file
                        --sign --cleanup --local-user --force --column --sort=
-                       --contains --points-at --merged --no-merged --create-reflog
+                       --contains --no-contains --points-at --merged --no-merged --create-reflog
                        "
                ;;
        esac
@@@ -3185,15 -3024,6 +3185,15 @@@ if [[ -n ${ZSH_VERSION-} ]]; the
                esac
        }
  
 +      __gitcomp_direct ()
 +      {
 +              emulate -L zsh
 +
 +              local IFS=$'\n'
 +              compset -P '*[=:]'
 +              compadd -Q -- ${=1} && _ret=0
 +      }
 +
        __gitcomp_nl ()
        {
                emulate -L zsh
diff --combined ref-filter.c
index 9c82b5b9d632bbbe66332b6c54badb5bc88d28d8,71b72e04e804995cd385608e8d4f7331cedd91b3..1e392730057ce97dab8be4b05dfa7c610e10fdce
@@@ -1298,9 -1298,9 +1298,9 @@@ static void populate_value(struct ref_a
        ref->value = xcalloc(used_atom_cnt, sizeof(struct atom_value));
  
        if (need_symref && (ref->flag & REF_ISSYMREF) && !ref->symref) {
 -              unsigned char unused1[20];
 +              struct object_id unused1;
                ref->symref = resolve_refdup(ref->refname, RESOLVE_REF_READING,
 -                                           unused1, NULL);
 +                                           unused1.hash, NULL);
                if (!ref->symref)
                        ref->symref = "";
        }
@@@ -1487,6 -1487,7 +1487,7 @@@ struct ref_filter_cbdata 
        struct ref_array *array;
        struct ref_filter *filter;
        struct contains_cache contains_cache;
+       struct contains_cache no_contains_cache;
  };
  
  /*
@@@ -1586,11 -1587,11 +1587,11 @@@ static enum contains_result contains_ta
  }
  
  static int commit_contains(struct ref_filter *filter, struct commit *commit,
-                          struct contains_cache *cache)
+                          struct commit_list *list, struct contains_cache *cache)
  {
        if (filter->with_commit_tag_algo)
-               return contains_tag_algo(commit, filter->with_commit, cache) == CONTAINS_YES;
-       return is_descendant_of(commit, filter->with_commit);
+               return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
+       return is_descendant_of(commit, list);
  }
  
  /*
@@@ -1780,13 -1781,17 +1781,17 @@@ static int ref_filter_handler(const cha
         * obtain the commit using the 'oid' available and discard all
         * non-commits early. The actual filtering is done later.
         */
-       if (filter->merge_commit || filter->with_commit || filter->verbose) {
+       if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) {
                commit = lookup_commit_reference_gently(oid->hash, 1);
                if (!commit)
                        return 0;
-               /* We perform the filtering for the '--contains' option */
+               /* We perform the filtering for the '--contains' option... */
                if (filter->with_commit &&
-                   !commit_contains(filter, commit, &ref_cbdata->contains_cache))
+                   !commit_contains(filter, commit, filter->with_commit, &ref_cbdata->contains_cache))
+                       return 0;
+               /* ...or for the `--no-contains' option */
+               if (filter->no_commit &&
+                   commit_contains(filter, commit, filter->no_commit, &ref_cbdata->no_contains_cache))
                        return 0;
        }
  
@@@ -1887,6 -1892,7 +1892,7 @@@ int filter_refs(struct ref_array *array
        filter->kind = type & FILTER_REFS_KIND_MASK;
  
        init_contains_cache(&ref_cbdata.contains_cache);
+       init_contains_cache(&ref_cbdata.no_contains_cache);
  
        /*  Simple per-ref filtering */
        if (!filter->kind)
        }
  
        clear_contains_cache(&ref_cbdata.contains_cache);
+       clear_contains_cache(&ref_cbdata.no_contains_cache);
  
        /*  Filters that need revision walking */
        if (filter->merge_commit)
@@@ -2084,8 -2091,17 +2091,17 @@@ int parse_opt_merge_filter(const struc
  {
        struct ref_filter *rf = opt->value;
        unsigned char sha1[20];
+       int no_merged = starts_with(opt->long_name, "no");
+       if (rf->merge) {
+               if (no_merged) {
+                       return opterror(opt, "is incompatible with --merged", 0);
+               } else {
+                       return opterror(opt, "is incompatible with --no-merged", 0);
+               }
+       }
  
-       rf->merge = starts_with(opt->long_name, "no")
+       rf->merge = no_merged
                ? REF_FILTER_MERGED_OMIT
                : REF_FILTER_MERGED_INCLUDE;
  
diff --combined t/t3200-branch.sh
index 9f353c0efce82e4492b0f3ce3acce1756827462e,f286f39b45995a047e3a5882feec390a9ec818cd..fe62e7c775da6a8fc191ed1dc6634d5285ddbc4b
@@@ -213,31 -213,6 +213,31 @@@ test_expect_success 'git branch --list 
        test_path_is_missing .git/refs/heads/t
  '
  
 +test_expect_success 'git branch --list -v with --abbrev' '
 +      test_when_finished "git branch -D t" &&
 +      git branch t &&
 +      git branch -v --list t >actual.default &&
 +      git branch -v --list --abbrev t >actual.abbrev &&
 +      test_cmp actual.default actual.abbrev &&
 +
 +      git branch -v --list --no-abbrev t >actual.noabbrev &&
 +      git branch -v --list --abbrev=0 t >actual.0abbrev &&
 +      test_cmp actual.noabbrev actual.0abbrev &&
 +
 +      git branch -v --list --abbrev=36 t >actual.36abbrev &&
 +      # how many hexdigits are used?
 +      read name objdefault rest <actual.abbrev &&
 +      read name obj36 rest <actual.36abbrev &&
 +      objfull=$(git rev-parse --verify t) &&
 +
 +      # are we really getting abbreviations?
 +      test "$obj36" != "$objdefault" &&
 +      expr "$obj36" : "$objdefault" >/dev/null &&
 +      test "$objfull" != "$obj36" &&
 +      expr "$objfull" : "$obj36" >/dev/null
 +
 +'
 +
  test_expect_success 'git branch --column' '
        COLUMNS=81 git branch --column=column >actual &&
        cat >expected <<\EOF &&
@@@ -978,6 -953,10 +978,10 @@@ test_expect_success '--merged catches i
        test_must_fail git branch --merged 0000000000000000000000000000000000000000
  '
  
+ test_expect_success '--merged is incompatible with --no-merged' '
+       test_must_fail git branch --merged HEAD --no-merged HEAD
+ '
  test_expect_success 'tracking with unexpected .fetch refspec' '
        rm -rf a b c d &&
        git init a &&
diff --combined t/t7004-tag.sh
index 772dc9ed960be0ea74bac52c571bb7a6f5b22053,6143113dbbd92a7468e86a5ee1dbc22ba78e3a17..bb2e4d704de006025ae0db9369b81bbebc15750a
@@@ -16,7 -16,6 +16,6 @@@ tag_exists () 
        git show-ref --quiet --verify refs/tags/"$1"
  }
  
- # todo: git tag -l now returns always zero, when fixed, change this test
  test_expect_success 'listing all tags in an empty tree should succeed' '
        git tag -l &&
        git tag
@@@ -119,6 -118,18 +118,18 @@@ test_expect_success 'listing all tags i
        git tag
  '
  
+ cat >expect <<EOF
+ mytag
+ EOF
+ test_expect_success 'Multiple -l or --list options are equivalent to one -l option' '
+       git tag -l -l >actual &&
+       test_cmp expect actual &&
+       git tag --list --list >actual &&
+       test_cmp expect actual &&
+       git tag --list -l --list >actual &&
+       test_cmp expect actual
+ '
  test_expect_success 'listing all tags if one exists should output that tag' '
        test $(git tag -l) = mytag &&
        test $(git tag) = mytag
@@@ -136,9 -147,8 +147,8 @@@ test_expect_success 
        'listing a tag using a matching pattern should output that tag' \
        'test $(git tag -l mytag) = mytag'
  
- # todo: git tag -l now returns always zero, when fixed, change this test
  test_expect_success \
-       'listing tags using a non-matching pattern should suceed' \
+       'listing tags using a non-matching pattern should succeed' \
        'git tag -l xxx'
  
  test_expect_success \
@@@ -338,6 -348,19 +348,19 @@@ test_expect_success 'tag -l can accept 
        test_cmp expect actual
  '
  
+ # Between v1.7.7 & v2.13.0 a fair reading of the git-tag documentation
+ # could leave you with the impression that "-l <pattern> -l <pattern>"
+ # was how we wanted to accept multiple patterns.
+ #
+ # This test should not imply that this is a sane thing to support. but
+ # since the documentation was worded like it was let's at least find
+ # out if we're going to break this long-documented form of taking
+ # multiple patterns.
+ test_expect_success 'tag -l <pattern> -l <pattern> works, as our buggy documentation previously suggested' '
+       git tag -l "v1*" -l "v0*" >actual &&
+       test_cmp expect actual
+ '
  test_expect_success 'listing tags in column' '
        COLUMNS=40 git tag -l --column=row >actual &&
        cat >expected <<\EOF &&
@@@ -620,6 -643,11 +643,11 @@@ test_expect_success 
        git tag -n0 -l tag-one-line >actual &&
        test_cmp expect actual &&
  
+       git tag -n0 | grep "^tag-one-line" >actual &&
+       test_cmp expect actual &&
+       git tag -n0 tag-one-line >actual &&
+       test_cmp expect actual &&
        echo "tag-one-line    A msg" >expect &&
        git tag -n1 -l | grep "^tag-one-line" >actual &&
        test_cmp expect actual &&
        test_cmp expect actual
  '
  
+ test_expect_success 'The -n 100 invocation means -n --list 100, not -n100' '
+       >expect &&
+       git tag -n 100 >actual &&
+       test_cmp expect actual &&
+       git tag -m "A msg" 100 &&
+       echo "100             A msg" >expect &&
+       git tag -n 100 >actual &&
+       test_cmp expect actual
+ '
  test_expect_success \
        'listing the zero-lines message of a non-signed tag should succeed' '
        git tag -m "" tag-zero-lines &&
@@@ -895,16 -934,18 +934,16 @@@ test_expect_success GPG 'verifying a fo
        test_must_fail git tag -v forged-tag
  '
  
 -test_expect_success 'verifying a proper tag with --format pass and format accordingly' '
 -      cat >expect <<-\EOF
 +test_expect_success GPG 'verifying a proper tag with --format pass and format accordingly' '
 +      cat >expect <<-\EOF &&
        tagname : signed-tag
 -      EOF &&
 +      EOF
        git tag -v --format="tagname : %(tag)" "signed-tag" >actual &&
        test_cmp expect actual
  '
  
 -test_expect_success 'verifying a forged tag with --format fail and format accordingly' '
 -      cat >expect <<-\EOF
 -      tagname : forged-tag
 -      EOF &&
 +test_expect_success GPG 'verifying a forged tag with --format should fail silently' '
 +      >expect &&
        test_must_fail git tag -v --format="tagname : %(tag)" "forged-tag" >actual &&
        test_cmp expect actual
  '
@@@ -1383,6 -1424,23 +1422,23 @@@ test_expect_success 'checking that firs
        test_cmp expected actual
  "
  
+ # All the --contains tests above, but with --no-contains
+ test_expect_success 'checking that first commit is not listed in any tag with --no-contains  (hash)' "
+       >expected &&
+       git tag -l --no-contains $hash1 v* >actual &&
+       test_cmp expected actual
+ "
+ test_expect_success 'checking that first commit is in all tags (tag)' "
+       git tag -l --no-contains v1.0 v* >actual &&
+       test_cmp expected actual
+ "
+ test_expect_success 'checking that first commit is in all tags (relative)' "
+       git tag -l --no-contains HEAD~2 v* >actual &&
+       test_cmp expected actual
+ "
  cat > expected <<EOF
  v2.0
  EOF
@@@ -1392,6 -1450,17 +1448,17 @@@ test_expect_success 'checking that seco
        test_cmp expected actual
  "
  
+ cat > expected <<EOF
+ v0.2.1
+ v1.0
+ v1.0.1
+ v1.1.3
+ EOF
+ test_expect_success 'inverse of the last test, with --no-contains' "
+       git tag -l --no-contains $hash2 v* >actual &&
+       test_cmp expected actual
+ "
  
  cat > expected <<EOF
  EOF
@@@ -1401,6 -1470,19 +1468,19 @@@ test_expect_success 'checking that thir
        test_cmp expected actual
  "
  
+ cat > expected <<EOF
+ v0.2.1
+ v1.0
+ v1.0.1
+ v1.1.3
+ v2.0
+ EOF
+ test_expect_success 'conversely --no-contains on the third commit lists all tags' "
+       git tag -l --no-contains $hash3 v* >actual &&
+       test_cmp expected actual
+ "
  # how about a simple merge?
  
  test_expect_success 'creating simple branch' '
@@@ -1422,6 -1504,19 +1502,19 @@@ test_expect_success 'checking that bran
        test_cmp expected actual
  "
  
+ cat > expected <<EOF
+ v0.2.1
+ v1.0
+ v1.0.1
+ v1.1.3
+ v2.0
+ EOF
+ test_expect_success 'checking that branch head with --no-contains lists all but one tag' "
+       git tag -l --no-contains $hash4 v* >actual &&
+       test_cmp expected actual
+ "
  test_expect_success 'merging original branch into this branch' '
        git merge --strategy=ours master &&
          git tag v4.0
@@@ -1436,6 -1531,20 +1529,20 @@@ test_expect_success 'checking that orig
        test_cmp expected actual
  "
  
+ cat > expected <<EOF
+ v0.2.1
+ v1.0
+ v1.0.1
+ v1.1.3
+ v2.0
+ v3.0
+ EOF
+ test_expect_success 'checking that original branch head with --no-contains lists all but one tag now' "
+       git tag -l --no-contains $hash3 v* >actual &&
+       test_cmp expected actual
+ "
  cat > expected <<EOF
  v0.2.1
  v1.0
@@@ -1451,21 -1560,76 +1558,76 @@@ test_expect_success 'checking that init
        test_cmp expected actual
  "
  
+ test_expect_success 'checking that --contains can be used in non-list mode' '
+       git tag --contains $hash1 v* >actual &&
+       test_cmp expected actual
+ '
+ test_expect_success 'checking that initial commit is in all tags with --no-contains' "
+       >expected &&
+       git tag -l --no-contains $hash1 v* >actual &&
+       test_cmp expected actual
+ "
  # mixing modes and options:
  
  test_expect_success 'mixing incompatibles modes and options is forbidden' '
        test_must_fail git tag -a &&
+       test_must_fail git tag -a -l &&
+       test_must_fail git tag -s &&
+       test_must_fail git tag -s -l &&
+       test_must_fail git tag -m &&
+       test_must_fail git tag -m -l &&
+       test_must_fail git tag -m "hlagh" &&
+       test_must_fail git tag -m "hlagh" -l &&
+       test_must_fail git tag -F &&
+       test_must_fail git tag -F -l &&
+       test_must_fail git tag -f &&
+       test_must_fail git tag -f -l &&
+       test_must_fail git tag -a -s -m -F &&
+       test_must_fail git tag -a -s -m -F -l &&
        test_must_fail git tag -l -v &&
-       test_must_fail git tag -n 100 &&
+       test_must_fail git tag -l -d &&
+       test_must_fail git tag -l -v -d &&
+       test_must_fail git tag -n 100 -v &&
        test_must_fail git tag -l -m msg &&
        test_must_fail git tag -l -F some file &&
-       test_must_fail git tag -v -s
- '
+       test_must_fail git tag -v -s &&
+       test_must_fail git tag --contains tag-tree &&
+       test_must_fail git tag --contains tag-blob &&
+       test_must_fail git tag --no-contains tag-tree &&
+       test_must_fail git tag --no-contains tag-blob &&
+       test_must_fail git tag --contains --no-contains &&
+       test_must_fail git tag --no-with HEAD &&
+       test_must_fail git tag --no-without HEAD
+ '
+ for option in --contains --with --no-contains --without --merged --no-merged --points-at
+ do
+       test_expect_success "mixing incompatible modes with $option is forbidden" "
+               test_must_fail git tag -d $option HEAD &&
+               test_must_fail git tag -d $option HEAD some-tag &&
+               test_must_fail git tag -v $option HEAD
+       "
+       test_expect_success "Doing 'git tag --list-like $option <commit> <pattern> is permitted" "
+               git tag -n $option HEAD HEAD &&
+               git tag $option HEAD HEAD &&
+               git tag $option
+       "
+ done
  
  # check points-at
  
- test_expect_success '--points-at cannot be used in non-list mode' '
-       test_must_fail git tag --points-at=v4.0 foo
+ test_expect_success '--points-at can be used in non-list mode' '
+       echo v4.0 >expect &&
+       git tag --points-at=v4.0 "v*" >actual &&
+       test_cmp expect actual
+ '
+ test_expect_success '--points-at is a synonym for --points-at HEAD' '
+       echo v4.0 >expect &&
+       git tag --points-at >actual &&
+       test_cmp expect actual
  '
  
  test_expect_success '--points-at finds lightweight tags' '
@@@ -1707,7 -1871,7 +1869,7 @@@ run_with_limited_stack () 
  test_lazy_prereq ULIMIT_STACK_SIZE 'run_with_limited_stack true'
  
  # we require ulimit, this excludes Windows
- test_expect_success ULIMIT_STACK_SIZE '--contains works in a deep repo' '
+ test_expect_success ULIMIT_STACK_SIZE '--contains and --no-contains work in a deep repo' '
        >expect &&
        i=1 &&
        while test $i -lt 8000
@@@ -1723,7 -1887,9 +1885,9 @@@ EOF
        git checkout master &&
        git tag far-far-away HEAD^ &&
        run_with_limited_stack git tag --contains HEAD >actual &&
-       test_cmp expect actual
+       test_cmp expect actual &&
+       run_with_limited_stack git tag --no-contains HEAD >actual &&
+       test_line_count ">" 10 actual
  '
  
  test_expect_success '--format should list tags as per format given' '
@@@ -1742,8 -1908,17 +1906,17 @@@ test_expect_success 'setup --merged tes
        git tag mergetest-3 HEAD
  '
  
- test_expect_success '--merged cannot be used in non-list mode' '
-       test_must_fail git tag --merged=mergetest-2 foo
+ test_expect_success '--merged can be used in non-list mode' '
+       cat >expect <<-\EOF &&
+       mergetest-1
+       mergetest-2
+       EOF
+       git tag --merged=mergetest-2 "mergetest*" >actual &&
+       test_cmp expect actual
+ '
+ test_expect_success '--merged is incompatible with --no-merged' '
+       test_must_fail git tag --merged HEAD --no-merged HEAD
  '
  
  test_expect_success '--merged shows merged tags' '
@@@ -1763,6 -1938,11 +1936,11 @@@ test_expect_success '--no-merged show u
        test_cmp expect actual
  '
  
+ test_expect_success '--no-merged can be used in non-list mode' '
+       git tag --no-merged=mergetest-2 mergetest-* >actual &&
+       test_cmp expect actual
+ '
  test_expect_success 'ambiguous branch/tags not marked' '
        git tag ambiguous &&
        git branch ambiguous &&
        test_cmp expect actual
  '
  
+ test_expect_success '--contains combined with --no-contains' '
+       (
+               git init no-contains &&
+               cd no-contains &&
+               test_commit v0.1 &&
+               test_commit v0.2 &&
+               test_commit v0.3 &&
+               test_commit v0.4 &&
+               test_commit v0.5 &&
+               cat >expected <<-\EOF &&
+               v0.2
+               v0.3
+               v0.4
+               EOF
+               git tag --contains v0.2 --no-contains v0.5 >actual &&
+               test_cmp expected actual
+       )
+ '
+ # As the docs say, list tags which contain a specified *commit*. We
+ # don't recurse down to tags for trees or blobs pointed to by *those*
+ # commits.
+ test_expect_success 'Does --[no-]contains stop at commits? Yes!' '
+       cd no-contains &&
+       blob=$(git rev-parse v0.3:v0.3.t) &&
+       tree=$(git rev-parse v0.3^{tree}) &&
+       git tag tag-blob $blob &&
+       git tag tag-tree $tree &&
+       git tag --contains v0.3 >actual &&
+       cat >expected <<-\EOF &&
+       v0.3
+       v0.4
+       v0.5
+       EOF
+       test_cmp expected actual &&
+       git tag --no-contains v0.3 >actual &&
+       cat >expected <<-\EOF &&
+       v0.1
+       v0.2
+       EOF
+       test_cmp expected actual
+ '
  test_done