Merge branch 'cn/fetch-prune-overlapping-destination' into maint
authorJunio C Hamano <gitster@pobox.com>
Wed, 9 Apr 2014 19:02:41 +0000 (12:02 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 9 Apr 2014 19:02:41 +0000 (12:02 -0700)
* 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 5f63d55056e7f1087766374a20a23e794bf3530a,fde7b52f9340fd5c2517f0f48532bb0f1f169ac8..8c15e9d913b520ee539695c556bb835b9c7c90a7
+++ 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
@@@ -1090,9 -1080,9 +1116,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;
@@@ -1140,7 -1130,7 +1166,7 @@@ static int match_explicit(struct ref *s
                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);
        }
@@@ -1229,7 -1219,7 +1255,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);
        }
@@@ -1284,7 -1274,7 +1310,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 */
@@@ -1517,7 -1507,7 +1543,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;
@@@ -1576,7 -1566,7 +1602,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;
@@@ -1629,7 -1612,7 +1655,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;
@@@ -1650,12 -1633,12 +1676,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);
@@@ -1690,7 -1673,7 +1716,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",
@@@ -1974,7 -1957,7 +2000,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);
@@@ -1997,25 -1980,37 +2023,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;
  }
  
@@@ -2126,7 -2121,7 +2164,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 ab28594c62dd7cddc69d3afdb075b48c34091c45,06161280b52a562bdcaf715a8e6e2b6dfcb2bd62..b212f83db7514a0d687cf9884e123a9b8dad7ae0
@@@ -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' '
@@@ -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^ &&