Merge branch 'cn/fetch-prune-overlapping-destination'
authorJunio C Hamano <gitster@pobox.com>
Thu, 3 Apr 2014 19:38:18 +0000 (12:38 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 3 Apr 2014 19:38:18 +0000 (12:38 -0700)
Protect refs in a hierarchy that can come from more than one remote
hierarcies from incorrect removal by "git fetch --prune".

* cn/fetch-prune-overlapping-destination:
fetch: handle overlaping refspecs on --prune
fetch: add a failing test for prunning with overlapping refspecs

1  2 
remote.c
t/t5510-fetch.sh
diff --combined remote.c
index 21b096932e033f0ebff92fc2224a79a132256ad2,fde7b52f9340fd5c2517f0f48532bb0f1f169ac8..6d424680c2987575d3958d42d540393586128ae9
+++ b/remote.c
@@@ -49,7 -49,6 +49,7 @@@ static int branches_nr
  
  static struct branch *current_branch;
  static const char *default_remote_name;
 +static const char *branch_pushremote_name;
  static const char *pushremote_name;
  static int explicit_default_remote_name;
  
@@@ -77,7 -76,7 +77,7 @@@ static const char *alias_url(const cha
                if (!r->rewrite[i])
                        continue;
                for (j = 0; j < r->rewrite[i]->instead_of_nr; j++) {
 -                      if (!prefixcmp(url, r->rewrite[i]->instead_of[j].s) &&
 +                      if (starts_with(url, r->rewrite[i]->instead_of[j].s) &&
                            (!longest ||
                             longest->len < r->rewrite[i]->instead_of[j].len)) {
                                longest = &(r->rewrite[i]->instead_of[j]);
@@@ -240,13 -239,13 +240,13 @@@ static void read_remotes_file(struct re
                int value_list;
                char *s, *p;
  
 -              if (!prefixcmp(buffer, "URL:")) {
 +              if (starts_with(buffer, "URL:")) {
                        value_list = 0;
                        s = buffer + 4;
 -              } else if (!prefixcmp(buffer, "Push:")) {
 +              } else if (starts_with(buffer, "Push:")) {
                        value_list = 1;
                        s = buffer + 5;
 -              } else if (!prefixcmp(buffer, "Pull:")) {
 +              } else if (starts_with(buffer, "Pull:")) {
                        value_list = 2;
                        s = buffer + 5;
                } else
@@@ -338,7 -337,7 +338,7 @@@ static int handle_config(const char *ke
        const char *subkey;
        struct remote *remote;
        struct branch *branch;
 -      if (!prefixcmp(key, "branch.")) {
 +      if (starts_with(key, "branch.")) {
                name = key + 7;
                subkey = strrchr(name, '.');
                if (!subkey)
                        }
                } else if (!strcmp(subkey, ".pushremote")) {
                        if (branch == current_branch)
 -                              if (git_config_string(&pushremote_name, key, value))
 +                              if (git_config_string(&branch_pushremote_name, key, value))
                                        return -1;
                } else if (!strcmp(subkey, ".merge")) {
                        if (!value)
                }
                return 0;
        }
 -      if (!prefixcmp(key, "url.")) {
 +      if (starts_with(key, "url.")) {
                struct rewrite *rewrite;
                name = key + 4;
                subkey = strrchr(name, '.');
                }
        }
  
 -      if (prefixcmp(key,  "remote."))
 +      if (!starts_with(key,  "remote."))
                return 0;
        name = key + 7;
  
@@@ -488,15 -487,11 +488,15 @@@ static void read_config(void
        current_branch = NULL;
        head_ref = resolve_ref_unsafe("HEAD", sha1, 0, &flag);
        if (head_ref && (flag & REF_ISSYMREF) &&
 -          !prefixcmp(head_ref, "refs/heads/")) {
 +          starts_with(head_ref, "refs/heads/")) {
                current_branch =
                        make_branch(head_ref + strlen("refs/heads/"), 0);
        }
        git_config(handle_config, NULL);
 +      if (branch_pushremote_name) {
 +              free((char *)pushremote_name);
 +              pushremote_name = branch_pushremote_name;
 +      }
        alias_all_urls();
  }
  
@@@ -750,66 -745,35 +750,66 @@@ int for_each_remote(each_remote_fn fn, 
        return result;
  }
  
 -void ref_remove_duplicates(struct ref *ref_map)
 +static void handle_duplicate(struct ref *ref1, struct ref *ref2)
 +{
 +      if (strcmp(ref1->name, ref2->name)) {
 +              if (ref1->fetch_head_status != FETCH_HEAD_IGNORE &&
 +                  ref2->fetch_head_status != FETCH_HEAD_IGNORE) {
 +                      die(_("Cannot fetch both %s and %s to %s"),
 +                          ref1->name, ref2->name, ref2->peer_ref->name);
 +              } else if (ref1->fetch_head_status != FETCH_HEAD_IGNORE &&
 +                         ref2->fetch_head_status == FETCH_HEAD_IGNORE) {
 +                      warning(_("%s usually tracks %s, not %s"),
 +                              ref2->peer_ref->name, ref2->name, ref1->name);
 +              } else if (ref1->fetch_head_status == FETCH_HEAD_IGNORE &&
 +                         ref2->fetch_head_status == FETCH_HEAD_IGNORE) {
 +                      die(_("%s tracks both %s and %s"),
 +                          ref2->peer_ref->name, ref1->name, ref2->name);
 +              } else {
 +                      /*
 +                       * This last possibility doesn't occur because
 +                       * FETCH_HEAD_IGNORE entries always appear at
 +                       * the end of the list.
 +                       */
 +                      die(_("Internal error"));
 +              }
 +      }
 +      free(ref2->peer_ref);
 +      free(ref2);
 +}
 +
 +struct ref *ref_remove_duplicates(struct ref *ref_map)
  {
        struct string_list refs = STRING_LIST_INIT_NODUP;
 -      struct string_list_item *item = NULL;
 -      struct ref *prev = NULL, *next = NULL;
 -      for (; ref_map; prev = ref_map, ref_map = next) {
 -              next = ref_map->next;
 -              if (!ref_map->peer_ref)
 -                      continue;
 +      struct ref *retval = NULL;
 +      struct ref **p = &retval;
  
 -              item = string_list_lookup(&refs, ref_map->peer_ref->name);
 -              if (item) {
 -                      if (strcmp(((struct ref *)item->util)->name,
 -                                 ref_map->name))
 -                              die("%s tracks both %s and %s",
 -                                  ref_map->peer_ref->name,
 -                                  ((struct ref *)item->util)->name,
 -                                  ref_map->name);
 -                      prev->next = ref_map->next;
 -                      free(ref_map->peer_ref);
 -                      free(ref_map);
 -                      ref_map = prev; /* skip this; we freed it */
 -                      continue;
 -              }
 +      while (ref_map) {
 +              struct ref *ref = ref_map;
 +
 +              ref_map = ref_map->next;
 +              ref->next = NULL;
  
 -              item = string_list_insert(&refs, ref_map->peer_ref->name);
 -              item->util = ref_map;
 +              if (!ref->peer_ref) {
 +                      *p = ref;
 +                      p = &ref->next;
 +              } else {
 +                      struct string_list_item *item =
 +                              string_list_insert(&refs, ref->peer_ref->name);
 +
 +                      if (item->util) {
 +                              /* Entry already existed */
 +                              handle_duplicate((struct ref *)item->util, ref);
 +                      } else {
 +                              *p = ref;
 +                              p = &ref->next;
 +                              item->util = ref;
 +                      }
 +              }
        }
 +
        string_list_clear(&refs, 0);
 +      return retval;
  }
  
  int remote_has_url(struct remote *remote, const char *url)
@@@ -857,12 -821,36 +857,38 @@@ static int match_name_with_pattern(cons
        return ret;
  }
  
 -static int query_refspecs(struct refspec *refs, int ref_count, struct refspec *query)
+ static void query_refspecs_multiple(struct refspec *refs, int ref_count, struct refspec *query, struct string_list *results)
+ {
+       int i;
+       int find_src = !query->src;
+       if (find_src && !query->dst)
+               error("query_refspecs_multiple: need either src or dst");
+       for (i = 0; i < ref_count; i++) {
+               struct refspec *refspec = &refs[i];
+               const char *key = find_src ? refspec->dst : refspec->src;
+               const char *value = find_src ? refspec->src : refspec->dst;
+               const char *needle = find_src ? query->dst : query->src;
+               char **result = find_src ? &query->src : &query->dst;
+               if (!refspec->dst)
+                       continue;
+               if (refspec->pattern) {
+                       if (match_name_with_pattern(key, needle, value, result))
+                               string_list_append_nodup(results, *result);
+               } else if (!strcmp(needle, key)) {
+                       string_list_append(results, value);
+               }
+       }
+ }
 +int query_refspecs(struct refspec *refs, int ref_count, struct refspec *query)
  {
        int i;
        int find_src = !query->src;
 +      const char *needle = find_src ? query->dst : query->src;
 +      char **result = find_src ? &query->src : &query->dst;
  
        if (find_src && !query->dst)
                return error("query_refspecs: need either src or dst");
                struct refspec *refspec = &refs[i];
                const char *key = find_src ? refspec->dst : refspec->src;
                const char *value = find_src ? refspec->src : refspec->dst;
 -              const char *needle = find_src ? query->dst : query->src;
 -              char **result = find_src ? &query->src : &query->dst;
  
                if (!refspec->dst)
                        continue;
@@@ -991,9 -981,9 +1017,9 @@@ void sort_ref_list(struct ref **l, int 
        *l = llist_mergesort(*l, ref_list_get_next, ref_list_set_next, cmp);
  }
  
 -static int count_refspec_match(const char *pattern,
 -                             struct ref *refs,
 -                             struct ref **matched_ref)
 +int count_refspec_match(const char *pattern,
 +                      struct ref *refs,
 +                      struct ref **matched_ref)
  {
        int patlen = strlen(pattern);
        struct ref *matched_weak = NULL;
                char *name = refs->name;
                int namelen = strlen(name);
  
 -              if (!refname_match(pattern, name, ref_rev_parse_rules))
 +              if (!refname_match(pattern, name))
                        continue;
  
                /* A match is "weak" if it is with refs outside
                 */
                if (namelen != patlen &&
                    patlen != namelen - 5 &&
 -                  prefixcmp(name, "refs/heads/") &&
 -                  prefixcmp(name, "refs/tags/")) {
 +                  !starts_with(name, "refs/heads/") &&
 +                  !starts_with(name, "refs/tags/")) {
                        /* We want to catch the case where only weak
                         * matches are found and there are multiple
                         * matches, and where more than one strong
                }
        }
        if (!matched) {
 -              *matched_ref = matched_weak;
 +              if (matched_ref)
 +                      *matched_ref = matched_weak;
                return weak_match;
        }
        else {
 -              *matched_ref = matched;
 +              if (matched_ref)
 +                      *matched_ref = matched;
                return match;
        }
  }
@@@ -1062,25 -1050,18 +1088,25 @@@ static struct ref *alloc_delete_ref(voi
        return ref;
  }
  
 -static struct ref *try_explicit_object_name(const char *name)
 +static int try_explicit_object_name(const char *name,
 +                                  struct ref **match)
  {
        unsigned char sha1[20];
 -      struct ref *ref;
  
 -      if (!*name)
 -              return alloc_delete_ref();
 +      if (!*name) {
 +              if (match)
 +                      *match = alloc_delete_ref();
 +              return 0;
 +      }
 +
        if (get_sha1(name, sha1))
 -              return NULL;
 -      ref = alloc_ref(name);
 -      hashcpy(ref->new_sha1, sha1);
 -      return ref;
 +              return -1;
 +
 +      if (match) {
 +              *match = alloc_ref(name);
 +              hashcpy((*match)->new_sha1, sha1);
 +      }
 +      return 0;
  }
  
  static struct ref *make_linked_ref(const char *name, struct ref ***tail)
@@@ -1099,9 -1080,9 +1125,9 @@@ static char *guess_ref(const char *name
        if (!r)
                return NULL;
  
 -      if (!prefixcmp(r, "refs/heads/"))
 +      if (starts_with(r, "refs/heads/"))
                strbuf_addstr(&buf, "refs/heads/");
 -      else if (!prefixcmp(r, "refs/tags/"))
 +      else if (starts_with(r, "refs/tags/"))
                strbuf_addstr(&buf, "refs/tags/");
        else
                return NULL;
        return strbuf_detach(&buf, NULL);
  }
  
 +static int match_explicit_lhs(struct ref *src,
 +                            struct refspec *rs,
 +                            struct ref **match,
 +                            int *allocated_match)
 +{
 +      switch (count_refspec_match(rs->src, src, match)) {
 +      case 1:
 +              if (allocated_match)
 +                      *allocated_match = 0;
 +              return 0;
 +      case 0:
 +              /* The source could be in the get_sha1() format
 +               * not a reference name.  :refs/other is a
 +               * way to delete 'other' ref at the remote end.
 +               */
 +              if (try_explicit_object_name(rs->src, match) < 0)
 +                      return error("src refspec %s does not match any.", rs->src);
 +              if (allocated_match)
 +                      *allocated_match = 1;
 +              return 0;
 +      default:
 +              return error("src refspec %s matches more than one.", rs->src);
 +      }
 +}
 +
  static int match_explicit(struct ref *src, struct ref *dst,
                          struct ref ***dst_tail,
                          struct refspec *rs)
  {
        struct ref *matched_src, *matched_dst;
 -      int copy_src;
 +      int allocated_src;
  
        const char *dst_value = rs->dst;
        char *dst_guess;
                return 0;
  
        matched_src = matched_dst = NULL;
 -      switch (count_refspec_match(rs->src, src, &matched_src)) {
 -      case 1:
 -              copy_src = 1;
 -              break;
 -      case 0:
 -              /* The source could be in the get_sha1() format
 -               * not a reference name.  :refs/other is a
 -               * way to delete 'other' ref at the remote end.
 -               */
 -              matched_src = try_explicit_object_name(rs->src);
 -              if (!matched_src)
 -                      return error("src refspec %s does not match any.", rs->src);
 -              copy_src = 0;
 -              break;
 -      default:
 -              return error("src refspec %s matches more than one.", rs->src);
 -      }
 +      if (match_explicit_lhs(src, rs, &matched_src, &allocated_src) < 0)
 +              return -1;
  
        if (!dst_value) {
                unsigned char sha1[20];
                dst_value = resolve_ref_unsafe(matched_src->name, sha1, 1, &flag);
                if (!dst_value ||
                    ((flag & REF_ISSYMREF) &&
 -                   prefixcmp(dst_value, "refs/heads/")))
 +                   !starts_with(dst_value, "refs/heads/")))
                        die("%s cannot be resolved to branch.",
                            matched_src->name);
        }
                return error("dst ref %s receives from more than one src.",
                      matched_dst->name);
        else {
 -              matched_dst->peer_ref = copy_src ? copy_ref(matched_src) : matched_src;
 +              matched_dst->peer_ref = allocated_src ?
 +                                      matched_src :
 +                                      copy_ref(matched_src);
                matched_dst->force = rs->force;
        }
        return 0;
@@@ -1250,7 -1219,7 +1276,7 @@@ static char *get_ref_match(const struc
                 * including refs outside refs/heads/ hierarchy, but
                 * that does not make much sense these days.
                 */
 -              if (!send_mirror && prefixcmp(ref->name, "refs/heads/"))
 +              if (!send_mirror && !starts_with(ref->name, "refs/heads/"))
                        return NULL;
                name = xstrdup(ref->name);
        }
@@@ -1305,7 -1274,7 +1331,7 @@@ static void add_missing_tags(struct re
                        add_to_tips(&sent_tips, ref->peer_ref->new_sha1);
                else
                        add_to_tips(&sent_tips, ref->old_sha1);
 -              if (!prefixcmp(ref->name, "refs/tags/"))
 +              if (starts_with(ref->name, "refs/tags/"))
                        string_list_append(&dst_tag, ref->name);
        }
        clear_commit_marks_many(sent_tips.nr, sent_tips.tip, TMP_MARK);
  
        /* Collect tags they do not have. */
        for (ref = src; ref; ref = ref->next) {
 -              if (prefixcmp(ref->name, "refs/tags/"))
 +              if (!starts_with(ref->name, "refs/tags/"))
                        continue; /* not a tag */
                if (string_list_has_string(&dst_tag, ref->name))
                        continue; /* they already have it */
@@@ -1378,31 -1347,6 +1404,31 @@@ static void prepare_ref_index(struct st
        sort_string_list(ref_index);
  }
  
 +/*
 + * Given only the set of local refs, sanity-check the set of push
 + * refspecs. We can't catch all errors that match_push_refs would,
 + * but we can catch some errors early before even talking to the
 + * remote side.
 + */
 +int check_push_refs(struct ref *src, int nr_refspec, const char **refspec_names)
 +{
 +      struct refspec *refspec = parse_push_refspec(nr_refspec, refspec_names);
 +      int ret = 0;
 +      int i;
 +
 +      for (i = 0; i < nr_refspec; i++) {
 +              struct refspec *rs = refspec + i;
 +
 +              if (rs->pattern || rs->matching)
 +                      continue;
 +
 +              ret |= match_explicit_lhs(src, rs, NULL, NULL);
 +      }
 +
 +      free_refspec(nr_refspec, refspec);
 +      return ret;
 +}
 +
  /*
   * Given the set of refs the local repository has, the set of refs the
   * remote repository has, and the refspec used for push, determine
@@@ -1563,7 -1507,7 +1589,7 @@@ void set_ref_status_for_push(struct re
                 */
  
                else if (!ref->deletion && !is_null_sha1(ref->old_sha1)) {
 -                      if (!prefixcmp(ref->name, "refs/tags/"))
 +                      if (starts_with(ref->name, "refs/tags/"))
                                reject_reason = REF_STATUS_REJECT_ALREADY_EXISTS;
                        else if (!has_sha1_file(ref->old_sha1))
                                reject_reason = REF_STATUS_REJECT_FETCH_FIRST;
@@@ -1622,7 -1566,7 +1648,7 @@@ int branch_merge_matches(struct branch 
  {
        if (!branch || i < 0 || i >= branch->merge_nr)
                return 0;
 -      return refname_match(branch->merge[i]->src, refname, ref_fetch_rules);
 +      return refname_match(branch->merge[i]->src, refname);
  }
  
  static int ignore_symref_update(const char *refname)
        return (flag & REF_ISSYMREF);
  }
  
 +/*
 + * Create and return a list of (struct ref) consisting of copies of
 + * each remote_ref that matches refspec.  refspec must be a pattern.
 + * Fill in the copies' peer_ref to describe the local tracking refs to
 + * which they map.  Omit any references that would map to an existing
 + * local symbolic ref.
 + */
  static struct ref *get_expanded_map(const struct ref *remote_refs,
                                    const struct refspec *refspec)
  {
        struct ref *ret = NULL;
        struct ref **tail = &ret;
  
 -      char *expn_name;
 -
        for (ref = remote_refs; ref; ref = ref->next) {
 +              char *expn_name = NULL;
 +
                if (strchr(ref->name, '^'))
                        continue; /* a dereference item */
                if (match_name_with_pattern(refspec->src, ref->name,
                        struct ref *cpy = copy_ref(ref);
  
                        cpy->peer_ref = alloc_ref(expn_name);
 -                      free(expn_name);
                        if (refspec->force)
                                cpy->peer_ref->force = 1;
                        *tail = cpy;
                        tail = &cpy->next;
                }
 +              free(expn_name);
        }
  
        return ret;
@@@ -1675,7 -1612,7 +1701,7 @@@ static const struct ref *find_ref_by_na
  {
        const struct ref *ref;
        for (ref = refs; ref; ref = ref->next) {
 -              if (refname_match(name, ref->name, ref_fetch_rules))
 +              if (refname_match(name, ref->name))
                        return ref;
        }
        return NULL;
@@@ -1696,12 -1633,12 +1722,12 @@@ static struct ref *get_local_ref(const 
        if (!name || name[0] == '\0')
                return NULL;
  
 -      if (!prefixcmp(name, "refs/"))
 +      if (starts_with(name, "refs/"))
                return alloc_ref(name);
  
 -      if (!prefixcmp(name, "heads/") ||
 -          !prefixcmp(name, "tags/") ||
 -          !prefixcmp(name, "remotes/"))
 +      if (starts_with(name, "heads/") ||
 +          starts_with(name, "tags/") ||
 +          starts_with(name, "remotes/"))
                return alloc_ref_with_prefix("refs/", 5, name);
  
        return alloc_ref_with_prefix("refs/heads/", 11, name);
@@@ -1736,7 -1673,7 +1762,7 @@@ int get_fetch_map(const struct ref *rem
  
        for (rmp = &ref_map; *rmp; ) {
                if ((*rmp)->peer_ref) {
 -                      if (prefixcmp((*rmp)->peer_ref->name, "refs/") ||
 +                      if (!starts_with((*rmp)->peer_ref->name, "refs/") ||
                            check_refname_format((*rmp)->peer_ref->name, 0)) {
                                struct ref *ignore = *rmp;
                                error("* Ignoring funny ref '%s' locally",
@@@ -2020,7 -1957,7 +2046,7 @@@ struct ref *guess_remote_head(const str
        /* Look for another ref that points there */
        for (r = refs; r; r = r->next) {
                if (r != head &&
 -                  !prefixcmp(r->name, "refs/heads/") &&
 +                  starts_with(r->name, "refs/heads/") &&
                    !hashcmp(r->old_sha1, head->old_sha1)) {
                        *tail = copy_ref(r);
                        tail = &((*tail)->next);
@@@ -2043,25 -1980,37 +2069,37 @@@ static int get_stale_heads_cb(const cha
        const unsigned char *sha1, int flags, void *cb_data)
  {
        struct stale_heads_info *info = cb_data;
+       struct string_list matches = STRING_LIST_INIT_DUP;
        struct refspec query;
+       int i, stale = 1;
        memset(&query, 0, sizeof(struct refspec));
        query.dst = (char *)refname;
  
-       if (query_refspecs(info->refs, info->ref_count, &query))
-               return 0; /* No matches */
+       query_refspecs_multiple(info->refs, info->ref_count, &query, &matches);
+       if (matches.nr == 0)
+               goto clean_exit; /* No matches */
  
        /*
         * If we did find a suitable refspec and it's not a symref and
         * it's not in the list of refs that currently exist in that
-        * remote we consider it to be stale.
+        * remote, we consider it to be stale. In order to deal with
+        * overlapping refspecs, we need to go over all of the
+        * matching refs.
         */
-       if (!((flags & REF_ISSYMREF) ||
-             string_list_has_string(info->ref_names, query.src))) {
+       if (flags & REF_ISSYMREF)
+               goto clean_exit;
+       for (i = 0; stale && i < matches.nr; i++)
+               if (string_list_has_string(info->ref_names, matches.items[i].string))
+                       stale = 0;
+       if (stale) {
                struct ref *ref = make_linked_ref(refname, &info->stale_refs_tail);
                hashcpy(ref->new_sha1, sha1);
        }
  
-       free(query.src);
+ clean_exit:
+       string_list_clear(&matches, 0);
        return 0;
  }
  
@@@ -2172,7 -2121,7 +2210,7 @@@ static void apply_cas(struct push_cas_o
        /* Find an explicit --<option>=<name>[:<value>] entry */
        for (i = 0; i < cas->nr; i++) {
                struct push_cas *entry = &cas->entry[i];
 -              if (!refname_match(entry->refname, ref->name, ref_rev_parse_rules))
 +              if (!refname_match(entry->refname, ref->name))
                        continue;
                ref->expect_old_sha1 = 1;
                if (!entry->use_tracking)
diff --combined t/t5510-fetch.sh
index 5acd753dcff6b8a0e908b82dc86295a7727fb4b4,06161280b52a562bdcaf715a8e6e2b6dfcb2bd62..29d59ef9fa2bf2bffb0075f07e03103387ab718e
@@@ -88,7 -88,7 +88,7 @@@ test_expect_success 'fetch --prune on i
        cd "$D" &&
        git clone . prune &&
        cd prune &&
 -      git fetch origin refs/heads/master:refs/remotes/origin/extrabranch &&
 +      git update-ref refs/remotes/origin/extrabranch master &&
  
        git fetch --prune origin &&
        test_must_fail git rev-parse origin/extrabranch
@@@ -98,7 -98,7 +98,7 @@@ test_expect_success 'fetch --prune wit
        cd "$D" &&
        git clone . prune-branch &&
        cd prune-branch &&
 -      git fetch origin refs/heads/master:refs/remotes/origin/extrabranch &&
 +      git update-ref refs/remotes/origin/extrabranch master &&
  
        git fetch --prune origin master &&
        git rev-parse origin/extrabranch
@@@ -113,45 -113,45 +113,65 @@@ test_expect_success 'fetch --prune wit
        git rev-parse origin/master
  '
  
 -test_expect_success 'fetch --prune --tags does not delete the remote-tracking branches' '
+ test_expect_success 'fetch --prune handles overlapping refspecs' '
+       cd "$D" &&
+       git update-ref refs/pull/42/head master &&
+       git clone . prune-overlapping &&
+       cd prune-overlapping &&
+       git config --add remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* &&
+       git fetch --prune origin &&
+       git rev-parse origin/master &&
+       git rev-parse origin/pr/42 &&
+       git config --unset-all remote.origin.fetch
+       git config remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* &&
+       git config --add remote.origin.fetch refs/heads/*:refs/remotes/origin/* &&
+       git fetch --prune origin &&
+       git rev-parse origin/master &&
+       git rev-parse origin/pr/42
+ '
 +test_expect_success 'fetch --prune --tags prunes branches but not tags' '
        cd "$D" &&
        git clone . prune-tags &&
        cd prune-tags &&
 -      git fetch origin refs/heads/master:refs/tags/sometag &&
 +      git tag sometag master &&
 +      # Create what looks like a remote-tracking branch from an earlier
 +      # fetch that has since been deleted from the remote:
 +      git update-ref refs/remotes/origin/fake-remote master &&
  
        git fetch --prune --tags origin &&
        git rev-parse origin/master &&
 -      test_must_fail git rev-parse somebranch
 +      test_must_fail git rev-parse origin/fake-remote &&
 +      git rev-parse sometag
  '
  
 -test_expect_success 'fetch --prune --tags with branch does not delete other remote-tracking branches' '
 +test_expect_success 'fetch --prune --tags with branch does not prune other things' '
        cd "$D" &&
        git clone . prune-tags-branch &&
        cd prune-tags-branch &&
 -      git fetch origin refs/heads/master:refs/remotes/origin/extrabranch &&
 +      git tag sometag master &&
 +      git update-ref refs/remotes/origin/extrabranch master &&
  
        git fetch --prune --tags origin master &&
 -      git rev-parse origin/extrabranch
 +      git rev-parse origin/extrabranch &&
 +      git rev-parse sometag
 +'
 +
 +test_expect_success 'fetch --prune --tags with refspec prunes based on refspec' '
 +      cd "$D" &&
 +      git clone . prune-tags-refspec &&
 +      cd prune-tags-refspec &&
 +      git tag sometag master &&
 +      git update-ref refs/remotes/origin/foo/otherbranch master &&
 +      git update-ref refs/remotes/origin/extrabranch master &&
 +
 +      git fetch --prune --tags origin refs/heads/foo/*:refs/remotes/origin/foo/* &&
 +      test_must_fail git rev-parse refs/remotes/origin/foo/otherbranch &&
 +      git rev-parse origin/extrabranch &&
 +      git rev-parse sometag
  '
  
  test_expect_success 'fetch tags when there is no tags' '
@@@ -301,7 -301,7 +321,7 @@@ test_expect_success 'fetch via rsync' 
        mkdir rsynced &&
        (cd rsynced &&
         git init --bare &&
 -       git fetch "rsync:$(pwd)/../.git" master:refs/heads/master &&
 +       git fetch "rsync:../.git" master:refs/heads/master &&
         git gc --prune &&
         test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
         git fsck --full)
@@@ -312,7 -312,7 +332,7 @@@ test_expect_success 'push via rsync' 
        (cd rsynced2 &&
         git init) &&
        (cd rsynced &&
 -       git push "rsync:$(pwd)/../rsynced2/.git" master) &&
 +       git push "rsync:../rsynced2/.git" master) &&
        (cd rsynced2 &&
         git gc --prune &&
         test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
@@@ -323,7 -323,7 +343,7 @@@ test_expect_success 'push via rsync' 
        mkdir rsynced3 &&
        (cd rsynced3 &&
         git init) &&
 -      git push --all "rsync:$(pwd)/rsynced3/.git" &&
 +      git push --all "rsync:rsynced3/.git" &&
        (cd rsynced3 &&
         test $(git rev-parse master) = $(cd .. && git rev-parse master) &&
         git fsck --full)
@@@ -614,32 -614,6 +634,32 @@@ test_expect_success 'all boundary commi
        test_bundle_object_count .git/objects/pack/pack-${pack##pack    }.pack 3
  '
  
 +test_expect_success 'fetch --prune prints the remotes url' '
 +      git branch goodbye &&
 +      git clone . only-prunes &&
 +      git branch -D goodbye &&
 +      (
 +              cd only-prunes &&
 +              git fetch --prune origin 2>&1 | head -n1 >../actual
 +      ) &&
 +      echo "From ${D}/." >expect &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'branchname D/F conflict resolved by --prune' '
 +      git branch dir/file &&
 +      git clone . prune-df-conflict &&
 +      git branch -D dir/file &&
 +      git branch dir &&
 +      (
 +              cd prune-df-conflict &&
 +              git fetch --prune &&
 +              git rev-parse origin/dir >../actual
 +      ) &&
 +      git rev-parse dir >expect &&
 +      test_cmp expect actual
 +'
 +
  test_expect_success 'fetching a one-level ref works' '
        test_commit extra &&
        git reset --hard HEAD^ &&