Merge branch 'wp/sha1-name-negative-match'
authorJunio C Hamano <gitster@pobox.com>
Wed, 10 Feb 2016 22:20:10 +0000 (14:20 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 10 Feb 2016 22:20:10 +0000 (14:20 -0800)
A new "<branch>^{/!-<pattern>}" notation can be used to name a
commit that is reachable from <branch> that does not match the
given <pattern>.

* wp/sha1-name-negative-match:
object name: introduce '^{/!-<negative pattern>}' notation
test for '!' handling in rev-parse's named commits

1  2 
Documentation/revisions.txt
sha1_name.c
t/t1511-rev-parse-caret.sh
index fa4bdb208cd86c603e1d7e430e753d5fe4768a13,e0cd03fba9c5840c0fd369b77189ef8db8d1b73c..19314e3b7f3eb7616be5ab88580020206285f880
@@@ -61,11 -61,11 +61,11 @@@ some output processing may assume ref n
  '@'::
    '@' alone is a shortcut for 'HEAD'.
  
 -'<refname>@\{<date>\}', e.g. 'master@\{yesterday\}', 'HEAD@\{5 minutes ago\}'::
 +'<refname>@{<date>}', e.g. 'master@\{yesterday\}', 'HEAD@{5 minutes ago}'::
    A ref followed by the suffix '@' with a date specification
    enclosed in a brace
 -  pair (e.g. '\{yesterday\}', '\{1 month 2 weeks 3 days 1 hour 1
 -  second ago\}' or '\{1979-02-26 18:30:00\}') specifies the value
 +  pair (e.g. '\{yesterday\}', '{1 month 2 weeks 3 days 1 hour 1
 +  second ago}' or '{1979-02-26 18:30:00}') specifies the value
    of the ref at a prior point in time.  This suffix may only be
    used immediately following a ref name and the ref must have an
    existing log ('$GIT_DIR/logs/<ref>'). Note that this looks up the state
@@@ -73,7 -73,7 +73,7 @@@
    'master' branch last week. If you want to look at commits made during
    certain times, see '--since' and '--until'.
  
 -'<refname>@\{<n>\}', e.g. 'master@\{1\}'::
 +'<refname>@{<n>}', e.g. 'master@\{1\}'::
    A ref followed by the suffix '@' with an ordinal specification
    enclosed in a brace pair (e.g. '\{1\}', '\{15\}') specifies
    the n-th prior value of that ref.  For example 'master@\{1\}'
    immediately following a ref name and the ref must have an existing
    log ('$GIT_DIR/logs/<refname>').
  
 -'@\{<n>\}', e.g. '@\{1\}'::
 +'@{<n>}', e.g. '@\{1\}'::
    You can use the '@' construct with an empty ref part to get at a
    reflog entry of the current branch. For example, if you are on
    branch 'blabla' then '@\{1\}' means the same as 'blabla@\{1\}'.
  
 -'@\{-<n>\}', e.g. '@\{-1\}'::
 -  The construct '@\{-<n>\}' means the <n>th branch/commit checked out
 +'@{-<n>}', e.g. '@{-1}'::
 +  The construct '@{-<n>}' means the <n>th branch/commit checked out
    before the current one.
  
  '<branchname>@\{upstream\}', e.g. 'master@\{upstream\}', '@\{u\}'::
    `branch.<name>.merge`).  A missing branchname defaults to the
    current one.
  
 +'<branchname>@\{push\}', e.g. 'master@\{push\}', '@\{push\}'::
 +  The suffix '@\{push}' reports the branch "where we would push to" if
 +  `git push` were run while `branchname` was checked out (or the current
 +  'HEAD' if no branchname is specified). Since our push destination is
 +  in a remote repository, of course, we report the local tracking branch
 +  that corresponds to that branch (i.e., something in 'refs/remotes/').
 ++
 +Here's an example to make it more clear:
 ++
 +------------------------------
 +$ git config push.default current
 +$ git config remote.pushdefault myfork
 +$ git checkout -b mybranch origin/master
 +
 +$ git rev-parse --symbolic-full-name @{upstream}
 +refs/remotes/origin/master
 +
 +$ git rev-parse --symbolic-full-name @{push}
 +refs/remotes/myfork/mybranch
 +------------------------------
 ++
 +Note in the example that we set up a triangular workflow, where we pull
 +from one location and push to another. In a non-triangular workflow,
 +'@\{push}' is the same as '@\{upstream}', and there is no need for it.
 +
  '<rev>{caret}', e.g. 'HEAD{caret}, v1.5.1{caret}0'::
    A suffix '{caret}' to a revision parameter means the first parent of
    that commit object.  '{caret}<n>' means the <n>th parent (i.e.
    '<rev>{caret}1{caret}1{caret}1'.  See below for an illustration of
    the usage of this form.
  
 -'<rev>{caret}\{<type>\}', e.g. 'v0.99.8{caret}\{commit\}'::
 +'<rev>{caret}{<type>}', e.g. 'v0.99.8{caret}\{commit\}'::
    A suffix '{caret}' followed by an object type name enclosed in
    brace pair means dereference the object at '<rev>' recursively until
    an object of type '<type>' is found or the object cannot be
@@@ -159,13 -134,13 +159,13 @@@ it does not have to be dereferenced eve
  'rev{caret}\{tag\}' can be used to ensure that 'rev' identifies an
  existing tag object.
  
 -'<rev>{caret}\{\}', e.g. 'v0.99.8{caret}\{\}'::
 +'<rev>{caret}{}', e.g. 'v0.99.8{caret}{}'::
    A suffix '{caret}' followed by an empty brace pair
    means the object could be a tag,
    and dereference the tag recursively until a non-tag object is
    found.
  
 -'<rev>{caret}\{/<text>\}', e.g. 'HEAD^{/fix nasty bug}'::
 +'<rev>{caret}{/<text>}', e.g. 'HEAD^{/fix nasty bug}'::
    A suffix '{caret}' to a revision parameter, followed by a brace
    pair that contains a text led by a slash,
    is the same as the ':/fix nasty bug' syntax below except that
    A colon, followed by a slash, followed by a text, names
    a commit whose commit message matches the specified regular expression.
    This name returns the youngest matching commit which is
-   reachable from any ref.  If the commit message starts with a
-   '!' you have to repeat that;  the special sequence ':/!',
-   followed by something else than '!', is reserved for now.
-   The regular expression can match any part of the commit message. To
-   match messages starting with a string, one can use e.g. ':/^foo'.
+   reachable from any ref. The regular expression can match any part of the
+   commit message. To match messages starting with a string, one can use
+   e.g. ':/^foo'. The special sequence ':/!' is reserved for modifiers to what
+   is matched. ':/!-foo' performs a negative match, while ':/!!foo' matches a
+   literal '!' character, followed by 'foo'. Any other sequence beginning with
+   ':/!' is reserved for now.
  
  '<rev>:<path>', e.g. 'HEAD:README', ':README', 'master:./README'::
    A suffix ':' followed by a path names the blob or tree
diff --combined sha1_name.c
index 892db21813bc484628e940274e742d103629b0ed,fc5c60d3f2702bb5ee997f53ca379cccff672361..89918ca158379a02368b0604b14465fa14855c8e
@@@ -6,7 -6,6 +6,7 @@@
  #include "tree-walk.h"
  #include "refs.h"
  #include "remote.h"
 +#include "dir.h"
  
  static int get_sha1_oneline(const char *, unsigned char *, struct commit_list *);
  
@@@ -96,15 -95,11 +96,15 @@@ static void find_short_object_filename(
        }
        fakeent->next = alt_odb_list;
  
 -      sprintf(hex, "%.2s", hex_pfx);
 +      xsnprintf(hex, sizeof(hex), "%.2s", hex_pfx);
        for (alt = fakeent; alt && !ds->ambiguous; alt = alt->next) {
                struct dirent *de;
                DIR *dir;
 -              sprintf(alt->name, "%.2s/", hex_pfx);
 +              /*
 +               * every alt_odb struct has 42 extra bytes after the base
 +               * for exactly this purpose
 +               */
 +              xsnprintf(alt->name, 42, "%.2s/", hex_pfx);
                dir = opendir(alt->base);
                if (!dir)
                        continue;
@@@ -372,13 -367,14 +372,13 @@@ int for_each_abbrev(const char *prefix
        return ds.ambiguous;
  }
  
 -const char *find_unique_abbrev(const unsigned char *sha1, int len)
 +int find_unique_abbrev_r(char *hex, const unsigned char *sha1, int len)
  {
        int status, exists;
 -      static char hex[41];
  
 -      memcpy(hex, sha1_to_hex(sha1), 40);
 +      sha1_to_hex_r(hex, sha1);
        if (len == 40 || !len)
 -              return hex;
 +              return 40;
        exists = has_sha1_file(sha1);
        while (len < 40) {
                unsigned char sha1_ret[20];
                    ? !status
                    : status == SHORT_NAME_NOT_FOUND) {
                        hex[len] = 0;
 -                      return hex;
 +                      return len;
                }
                len++;
        }
 +      return len;
 +}
 +
 +const char *find_unique_abbrev(const unsigned char *sha1, int len)
 +{
 +      static char hex[GIT_SHA1_HEXSZ + 1];
 +      find_unique_abbrev_r(hex, sha1, len);
        return hex;
  }
  
@@@ -426,12 -415,12 +426,12 @@@ static int ambiguous_path(const char *p
        return slash;
  }
  
 -static inline int upstream_mark(const char *string, int len)
 +static inline int at_mark(const char *string, int len,
 +                        const char **suffix, int nr)
  {
 -      const char *suffix[] = { "@{upstream}", "@{u}" };
        int i;
  
 -      for (i = 0; i < ARRAY_SIZE(suffix); i++) {
 +      for (i = 0; i < nr; i++) {
                int suffix_len = strlen(suffix[i]);
                if (suffix_len <= len
                    && !memcmp(string, suffix[i], suffix_len))
        return 0;
  }
  
 +static inline int upstream_mark(const char *string, int len)
 +{
 +      const char *suffix[] = { "@{upstream}", "@{u}" };
 +      return at_mark(string, len, suffix, ARRAY_SIZE(suffix));
 +}
 +
 +static inline int push_mark(const char *string, int len)
 +{
 +      const char *suffix[] = { "@{push}" };
 +      return at_mark(string, len, suffix, ARRAY_SIZE(suffix));
 +}
 +
  static int get_sha1_1(const char *name, int len, unsigned char *sha1, unsigned lookup_flags);
  static int interpret_nth_prior_checkout(const char *name, int namelen, struct strbuf *buf);
  
@@@ -499,8 -476,7 +499,8 @@@ static int get_sha1_basic(const char *s
                                        nth_prior = 1;
                                        continue;
                                }
 -                              if (!upstream_mark(str + at, len - at)) {
 +                              if (!upstream_mark(str + at, len - at) &&
 +                                  !push_mark(str + at, len - at)) {
                                        reflog_len = (len-1) - (at+2);
                                        len = at;
                                }
                                if (!(flags & GET_SHA1_QUIETLY)) {
                                        warning("Log for '%.*s' only goes "
                                                "back to %s.", len, str,
 -                                              show_date(co_time, co_tz, DATE_RFC2822));
 +                                              show_date(co_time, co_tz, DATE_MODE(RFC2822)));
                                }
                        } else {
                                if (flags & GET_SHA1_QUIETLY) {
@@@ -616,13 -592,13 +616,13 @@@ static int get_parent(const char *name
        if (parse_commit(commit))
                return -1;
        if (!idx) {
 -              hashcpy(result, commit->object.sha1);
 +              hashcpy(result, commit->object.oid.hash);
                return 0;
        }
        p = commit->parents;
        while (p) {
                if (!--idx) {
 -                      hashcpy(result, p->item->object.sha1);
 +                      hashcpy(result, p->item->object.oid.hash);
                        return 0;
                }
                p = p->next;
@@@ -649,7 -625,7 +649,7 @@@ static int get_nth_ancestor(const char 
                        return -1;
                commit = commit->parents->item;
        }
 -      hashcpy(result, commit->object.sha1);
 +      hashcpy(result, commit->object.oid.hash);
        return 0;
  }
  
@@@ -659,7 -635,7 +659,7 @@@ struct object *peel_to_type(const char 
        if (name && !namelen)
                namelen = strlen(name);
        while (1) {
 -              if (!o || (!o->parsed && !parse_object(o->sha1)))
 +              if (!o || (!o->parsed && !parse_object(o->oid.hash)))
                        return NULL;
                if (expected_type == OBJ_ANY || o->type == expected_type)
                        return o;
@@@ -736,9 -712,9 +736,9 @@@ static int peel_onion(const char *name
                return -1;
        if (!expected_type) {
                o = deref_tag(o, name, sp - name - 2);
 -              if (!o || (!o->parsed && !parse_object(o->sha1)))
 +              if (!o || (!o->parsed && !parse_object(o->oid.hash)))
                        return -1;
 -              hashcpy(sha1, o->sha1);
 +              hashcpy(sha1, o->oid.hash);
                return 0;
        }
  
        if (!o)
                return -1;
  
 -      hashcpy(sha1, o->sha1);
 +      hashcpy(sha1, o->oid.hash);
        if (sp[0] == '/') {
                /* "$commit^{/foo}" */
                char *prefix;
@@@ -848,18 -824,22 +848,22 @@@ static int get_sha1_1(const char *name
   * through history and returning the first commit whose message starts
   * the given regular expression.
   *
-  * For future extension, ':/!' is reserved. If you want to match a message
-  * beginning with a '!', you have to repeat the exclamation mark.
+  * For negative-matching, prefix the pattern-part with '!-', like: ':/!-WIP'.
+  *
+  * For a literal '!' character at the beginning of a pattern, you have to repeat
+  * that, like: ':/!!foo'
+  *
+  * For future extension, all other sequences beginning with ':/!' are reserved.
   */
  
  /* Remember to update object flag allocation in object.h */
  #define ONELINE_SEEN (1u<<20)
  
 -static int handle_one_ref(const char *path,
 -              const unsigned char *sha1, int flag, void *cb_data)
 +static int handle_one_ref(const char *path, const struct object_id *oid,
 +                        int flag, void *cb_data)
  {
        struct commit_list **list = cb_data;
 -      struct object *object = parse_object(sha1);
 +      struct object *object = parse_object(oid->hash);
        if (!object)
                return 0;
        if (object->type == OBJ_TAG) {
@@@ -878,12 -858,18 +882,18 @@@ static int get_sha1_oneline(const char 
  {
        struct commit_list *backup = NULL, *l;
        int found = 0;
+       int negative = 0;
        regex_t regex;
  
        if (prefix[0] == '!') {
-               if (prefix[1] != '!')
-                       die ("Invalid search pattern: %s", prefix);
                prefix++;
+               if (prefix[0] == '-') {
+                       prefix++;
+                       negative = 1;
+               } else if (prefix[0] != '!') {
+                       die ("Invalid search pattern: %s", prefix);
+               }
        }
  
        if (regcomp(&regex, prefix, REG_EXTENDED))
                int matches;
  
                commit = pop_most_recent_commit(&list, ONELINE_SEEN);
 -              if (!parse_object(commit->object.sha1))
 +              if (!parse_object(commit->object.oid.hash))
                        continue;
                buf = get_commit_buffer(commit, NULL);
                p = strstr(buf, "\n\n");
-               matches = p && !regexec(&regex, p + 2, 0, NULL, 0);
+               matches = negative ^ (p && !regexec(&regex, p + 2, 0, NULL, 0));
                unuse_commit_buffer(commit, buf);
  
                if (matches) {
 -                      hashcpy(sha1, commit->object.sha1);
 +                      hashcpy(sha1, commit->object.oid.hash);
                        found = 1;
                        break;
                }
@@@ -1022,7 -1008,7 +1032,7 @@@ int get_sha1_mb(const char *name, unsig
                st = -1;
        else {
                st = 0;
 -              hashcpy(sha1, mbs->item->object.sha1);
 +              hashcpy(sha1, mbs->item->object.oid.hash);
        }
        free_commit_list(mbs);
        return st;
@@@ -1079,36 -1065,46 +1089,36 @@@ static void set_shortened_ref(struct st
        free(s);
  }
  
 -static const char *get_upstream_branch(const char *branch_buf, int len)
 -{
 -      char *branch = xstrndup(branch_buf, len);
 -      struct branch *upstream = branch_get(*branch ? branch : NULL);
 -
 -      /*
 -       * Upstream can be NULL only if branch refers to HEAD and HEAD
 -       * points to something different than a branch.
 -       */
 -      if (!upstream)
 -              die(_("HEAD does not point to a branch"));
 -      if (!upstream->merge || !upstream->merge[0]->dst) {
 -              if (!ref_exists(upstream->refname))
 -                      die(_("No such branch: '%s'"), branch);
 -              if (!upstream->merge) {
 -                      die(_("No upstream configured for branch '%s'"),
 -                              upstream->name);
 -              }
 -              die(
 -                      _("Upstream branch '%s' not stored as a remote-tracking branch"),
 -                      upstream->merge[0]->src);
 -      }
 -      free(branch);
 -
 -      return upstream->merge[0]->dst;
 -}
 -
 -static int interpret_upstream_mark(const char *name, int namelen,
 -                                 int at, struct strbuf *buf)
 +static int interpret_branch_mark(const char *name, int namelen,
 +                               int at, struct strbuf *buf,
 +                               int (*get_mark)(const char *, int),
 +                               const char *(*get_data)(struct branch *,
 +                                                       struct strbuf *))
  {
        int len;
 +      struct branch *branch;
 +      struct strbuf err = STRBUF_INIT;
 +      const char *value;
  
 -      len = upstream_mark(name + at, namelen - at);
 +      len = get_mark(name + at, namelen - at);
        if (!len)
                return -1;
  
        if (memchr(name, ':', at))
                return -1;
  
 -      set_shortened_ref(buf, get_upstream_branch(name, at));
 +      if (at) {
 +              char *name_str = xmemdupz(name, at);
 +              branch = branch_get(name_str);
 +              free(name_str);
 +      } else
 +              branch = branch_get(NULL);
 +
 +      value = get_data(branch, &err);
 +      if (!value)
 +              die("%s", err.buf);
 +
 +      set_shortened_ref(buf, value);
        return len + at;
  }
  
@@@ -1159,13 -1155,7 +1169,13 @@@ int interpret_branch_name(const char *n
                if (len > 0)
                        return reinterpret(name, namelen, len, buf);
  
 -              len = interpret_upstream_mark(name, namelen, at - name, buf);
 +              len = interpret_branch_mark(name, namelen, at - name, buf,
 +                                          upstream_mark, branch_get_upstream);
 +              if (len > 0)
 +                      return len;
 +
 +              len = interpret_branch_mark(name, namelen, at - name, buf,
 +                                          push_mark, branch_get_push);
                if (len > 0)
                        return len;
        }
@@@ -1257,13 -1247,14 +1267,13 @@@ static void diagnose_invalid_sha1_path(
                                       const char *object_name,
                                       int object_name_len)
  {
 -      struct stat st;
        unsigned char sha1[20];
        unsigned mode;
  
        if (!prefix)
                prefix = "";
  
 -      if (!lstat(filename, &st))
 +      if (file_exists(filename))
                die("Path '%s' exists on disk, but not in '%.*s'.",
                    filename, object_name_len, object_name);
        if (errno == ENOENT || errno == ENOTDIR) {
@@@ -1290,10 -1281,12 +1300,10 @@@ static void diagnose_invalid_index_path
                                        const char *prefix,
                                        const char *filename)
  {
 -      struct stat st;
        const struct cache_entry *ce;
        int pos;
        unsigned namelen = strlen(filename);
 -      unsigned fullnamelen;
 -      char *fullname;
 +      struct strbuf fullname = STRBUF_INIT;
  
        if (!prefix)
                prefix = "";
        }
  
        /* 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);
 +      strbuf_addstr(&fullname, prefix);
 +      strbuf_addstr(&fullname, filename);
 +      pos = cache_name_pos(fullname.buf, fullname.len);
        if (pos < 0)
                pos = -pos - 1;
        if (pos < active_nr) {
                ce = active_cache[pos];
 -              if (ce_namelen(ce) == fullnamelen &&
 -                  !memcmp(ce->name, fullname, fullnamelen))
 +              if (ce_namelen(ce) == fullname.len &&
 +                  !memcmp(ce->name, fullname.buf, fullname.len))
                        die("Path '%s' is in the index, but not '%s'.\n"
                            "Did you mean ':%d:%s' aka ':%d:./%s'?",
 -                          fullname, filename,
 -                          ce_stage(ce), fullname,
 +                          fullname.buf, filename,
 +                          ce_stage(ce), fullname.buf,
                            ce_stage(ce), filename);
        }
  
 -      if (!lstat(filename, &st))
 +      if (file_exists(filename))
                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);
 +      strbuf_release(&fullname);
  }
  
  
@@@ -1386,7 -1381,6 +1396,7 @@@ static int get_sha1_with_context_1(cons
                int pos;
                if (!only_to_die && namelen > 2 && name[1] == '/') {
                        struct commit_list *list = NULL;
 +
                        for_each_ref(handle_one_ref, &list);
                        commit_list_sort_by_date(&list);
                        return get_sha1_oneline(name + 2, sha1, list);
                        new_filename = resolve_relative_path(filename);
                        if (new_filename)
                                filename = new_filename;
 -                      ret = get_tree_entry(tree_sha1, filename, sha1, &oc->mode);
 -                      if (ret && only_to_die) {
 -                              diagnose_invalid_sha1_path(prefix, filename,
 -                                                         tree_sha1,
 -                                                         name, len);
 +                      if (flags & GET_SHA1_FOLLOW_SYMLINKS) {
 +                              ret = get_tree_entry_follow_symlinks(tree_sha1,
 +                                      filename, sha1, &oc->symlink_path,
 +                                      &oc->mode);
 +                      } else {
 +                              ret = get_tree_entry(tree_sha1, filename,
 +                                                   sha1, &oc->mode);
 +                              if (ret && only_to_die) {
 +                                      diagnose_invalid_sha1_path(prefix,
 +                                                                 filename,
 +                                                                 tree_sha1,
 +                                                                 name, len);
 +                              }
                        }
                        hashcpy(oc->tree, tree_sha1);
                        strlcpy(oc->path, filename, sizeof(oc->path));
@@@ -1493,7 -1479,5 +1503,7 @@@ void maybe_die_on_misspelt_object_name(
  
  int get_sha1_with_context(const char *str, unsigned flags, unsigned char *sha1, struct object_context *orc)
  {
 +      if (flags & GET_SHA1_FOLLOW_SYMLINKS && flags & GET_SHA1_ONLY_TO_DIE)
 +              die("BUG: incompatible flags for get_sha1_with_context");
        return get_sha1_with_context_1(str, flags, NULL, sha1, orc);
  }
index 7043ba7947933644f15d086d8bdd95d377c4d265,8a5983fcd29d4bb36ef4590eff0adb0e817a6286..e0a49a651fdd3b3f7d9e0f24e00439c9642f896c
@@@ -6,11 -6,11 +6,11 @@@ test_description='tests for ref^{stuff}
  
  test_expect_success 'setup' '
        echo blob >a-blob &&
 -      git tag -a -m blob blob-tag `git hash-object -w a-blob` &&
 +      git tag -a -m blob blob-tag $(git hash-object -w a-blob) &&
        mkdir a-tree &&
        echo moreblobs >a-tree/another-blob &&
        git add . &&
 -      TREE_SHA1=`git write-tree` &&
 +      TREE_SHA1=$(git write-tree) &&
        git tag -a -m tree tree-tag "$TREE_SHA1" &&
        git commit -m Initial &&
        git tag -a -m commit commit-tag &&
        git checkout master &&
        echo modified >>a-blob &&
        git add -u &&
-       git commit -m Modified
+       git commit -m Modified &&
+       git branch modref &&
+       echo changed! >>a-blob &&
+       git add -u &&
+       git commit -m !Exp &&
+       git branch expref &&
+       echo changed >>a-blob &&
+       git add -u &&
+       git commit -m Changed &&
+       echo changed-again >>a-blob &&
+       git add -u &&
+       git commit -m Changed-again
  '
  
  test_expect_success 'ref^{non-existent}' '
@@@ -77,4 -88,44 +88,44 @@@ test_expect_success 'ref^{/Initial}' 
        test_cmp expected actual
  '
  
+ test_expect_success 'ref^{/!Exp}' '
+       test_must_fail git rev-parse master^{/!Exp}
+ '
+ test_expect_success 'ref^{/!}' '
+       test_must_fail git rev-parse master^{/!}
+ '
+ test_expect_success 'ref^{/!!Exp}' '
+       git rev-parse expref >expected &&
+       git rev-parse master^{/!!Exp} >actual &&
+       test_cmp expected actual
+ '
+ test_expect_success 'ref^{/!-}' '
+       test_must_fail git rev-parse master^{/!-}
+ '
+ test_expect_success 'ref^{/!-.}' '
+       test_must_fail git rev-parse master^{/!-.}
+ '
+ test_expect_success 'ref^{/!-non-existent}' '
+       git rev-parse master >expected &&
+       git rev-parse master^{/!-non-existent} >actual &&
+       test_cmp expected actual
+ '
+ test_expect_success 'ref^{/!-Changed}' '
+       git rev-parse expref >expected &&
+       git rev-parse master^{/!-Changed} >actual &&
+       test_cmp expected actual
+ '
+ test_expect_success 'ref^{/!-!Exp}' '
+       git rev-parse modref >expected &&
+       git rev-parse expref^{/!-!Exp} >actual &&
+       test_cmp expected actual
+ '
  test_done