Merge branch 'js/maint-remote-remove-mirror'
authorJunio C Hamano <gitster@pobox.com>
Fri, 6 Feb 2009 03:40:41 +0000 (19:40 -0800)
committerJunio C Hamano <gitster@pobox.com>
Fri, 6 Feb 2009 03:40:41 +0000 (19:40 -0800)
* js/maint-remote-remove-mirror:
builtin-remote: make rm operation safer in mirrored repository
builtin-remote: make rm() use properly named variable to hold return value

1  2 
builtin-remote.c
t/t5505-remote.sh
diff --combined builtin-remote.c
index abc8dd8389be4a51b467b4f6d4f74e2037d65423,07cfdac46446dcffa8d48ba962816440526e7ebf..db18bcfc97f0739c2e77164dc0222148a44c67a6
@@@ -8,13 -8,12 +8,13 @@@
  #include "refs.h"
  
  static const char * const builtin_remote_usage[] = {
 -      "git remote",
 -      "git remote add <name> <url>",
 +      "git remote [-v | --verbose]",
 +      "git remote add [-t <branch>] [-m <master>] [-f] [--mirror] <name> <url>",
 +      "git remote rename <old> <new>",
        "git remote rm <name>",
 -      "git remote show <name>",
 -      "git remote prune <name>",
 -      "git remote update [group]",
 +      "git remote show [-n] <name>",
 +      "git remote prune [-n | --dry-run] <name>",
 +      "git remote [-v | --verbose] update [group]",
        NULL
  };
  
@@@ -42,11 -41,7 +42,11 @@@ static int opt_parse_track(const struc
  
  static int fetch_remote(const char *name)
  {
 -      const char *argv[] = { "fetch", name, NULL };
 +      const char *argv[] = { "fetch", name, NULL, NULL };
 +      if (verbose) {
 +              argv[1] = "-v";
 +              argv[2] = name;
 +      }
        printf("Updating %s\n", name);
        if (run_command_v_opt(argv, RUN_GIT_CMD))
                return error("Could not fetch %s", name);
@@@ -59,7 -54,7 +59,7 @@@ static int add(int argc, const char **a
        struct string_list track = { NULL, 0, 0 };
        const char *master = NULL;
        struct remote *remote;
 -      struct strbuf buf, buf2;
 +      struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
        const char *name, *url;
        int i;
  
@@@ -86,6 -81,9 +86,6 @@@
                        remote->fetch_refspec_nr))
                die("remote %s already exists.", name);
  
 -      strbuf_init(&buf, 0);
 -      strbuf_init(&buf2, 0);
 -
        strbuf_addf(&buf2, "refs/heads/test:refs/remotes/%s/test", name);
        if (!valid_fetch_refspec(buf2.buf))
                die("'%s' is not a valid remote name", name);
@@@ -298,7 -296,7 +298,7 @@@ static int add_known_remote(struct remo
  
  struct branches_for_remote {
        struct remote *remote;
-       struct string_list *branches;
+       struct string_list *branches, *skipped;
        struct known_remotes *keep;
  };
  
@@@ -323,6 -321,16 +323,16 @@@ static int add_branch_for_removal(cons
                        return 0;
        }
  
+       /* don't delete non-remote refs */
+       if (prefixcmp(refname, "refs/remotes")) {
+               /* advise user how to delete local branches */
+               if (!prefixcmp(refname, "refs/heads/"))
+                       string_list_append(abbrev_branch(refname),
+                                          branches->skipped);
+               /* silently skip over other non-remote refs */
+               return 0;
+       }
        /* make sure that symrefs are deleted */
        if (flags & REF_ISSYMREF)
                return unlink(git_path("%s", refname));
        return 0;
  }
  
 +struct rename_info {
 +      const char *old;
 +      const char *new;
 +      struct string_list *remote_branches;
 +};
 +
 +static int read_remote_branches(const char *refname,
 +      const unsigned char *sha1, int flags, void *cb_data)
 +{
 +      struct rename_info *rename = cb_data;
 +      struct strbuf buf = STRBUF_INIT;
 +      struct string_list_item *item;
 +      int flag;
 +      unsigned char orig_sha1[20];
 +      const char *symref;
 +
 +      strbuf_addf(&buf, "refs/remotes/%s", rename->old);
 +      if(!prefixcmp(refname, buf.buf)) {
 +              item = string_list_append(xstrdup(refname), rename->remote_branches);
 +              symref = resolve_ref(refname, orig_sha1, 1, &flag);
 +              if (flag & REF_ISSYMREF)
 +                      item->util = xstrdup(symref);
 +              else
 +                      item->util = NULL;
 +      }
 +
 +      return 0;
 +}
 +
 +static int migrate_file(struct remote *remote)
 +{
 +      struct strbuf buf = STRBUF_INIT;
 +      int i;
 +      char *path = NULL;
 +
 +      strbuf_addf(&buf, "remote.%s.url", remote->name);
 +      for (i = 0; i < remote->url_nr; i++)
 +              if (git_config_set_multivar(buf.buf, remote->url[i], "^$", 0))
 +                      return error("Could not append '%s' to '%s'",
 +                                      remote->url[i], buf.buf);
 +      strbuf_reset(&buf);
 +      strbuf_addf(&buf, "remote.%s.push", remote->name);
 +      for (i = 0; i < remote->push_refspec_nr; i++)
 +              if (git_config_set_multivar(buf.buf, remote->push_refspec[i], "^$", 0))
 +                      return error("Could not append '%s' to '%s'",
 +                                      remote->push_refspec[i], buf.buf);
 +      strbuf_reset(&buf);
 +      strbuf_addf(&buf, "remote.%s.fetch", remote->name);
 +      for (i = 0; i < remote->fetch_refspec_nr; i++)
 +              if (git_config_set_multivar(buf.buf, remote->fetch_refspec[i], "^$", 0))
 +                      return error("Could not append '%s' to '%s'",
 +                                      remote->fetch_refspec[i], buf.buf);
 +      if (remote->origin == REMOTE_REMOTES)
 +              path = git_path("remotes/%s", remote->name);
 +      else if (remote->origin == REMOTE_BRANCHES)
 +              path = git_path("branches/%s", remote->name);
 +      if (path && unlink(path))
 +              warning("failed to remove '%s'", path);
 +      return 0;
 +}
 +
 +static int mv(int argc, const char **argv)
 +{
 +      struct option options[] = {
 +              OPT_END()
 +      };
 +      struct remote *oldremote, *newremote;
 +      struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT;
 +      struct string_list remote_branches = { NULL, 0, 0, 0 };
 +      struct rename_info rename;
 +      int i;
 +
 +      if (argc != 3)
 +              usage_with_options(builtin_remote_usage, options);
 +
 +      rename.old = argv[1];
 +      rename.new = argv[2];
 +      rename.remote_branches = &remote_branches;
 +
 +      oldremote = remote_get(rename.old);
 +      if (!oldremote)
 +              die("No such remote: %s", rename.old);
 +
 +      if (!strcmp(rename.old, rename.new) && oldremote->origin != REMOTE_CONFIG)
 +              return migrate_file(oldremote);
 +
 +      newremote = remote_get(rename.new);
 +      if (newremote && (newremote->url_nr > 1 || newremote->fetch_refspec_nr))
 +              die("remote %s already exists.", rename.new);
 +
 +      strbuf_addf(&buf, "refs/heads/test:refs/remotes/%s/test", rename.new);
 +      if (!valid_fetch_refspec(buf.buf))
 +              die("'%s' is not a valid remote name", rename.new);
 +
 +      strbuf_reset(&buf);
 +      strbuf_addf(&buf, "remote.%s", rename.old);
 +      strbuf_addf(&buf2, "remote.%s", rename.new);
 +      if (git_config_rename_section(buf.buf, buf2.buf) < 1)
 +              return error("Could not rename config section '%s' to '%s'",
 +                              buf.buf, buf2.buf);
 +
 +      strbuf_reset(&buf);
 +      strbuf_addf(&buf, "remote.%s.fetch", rename.new);
 +      if (git_config_set_multivar(buf.buf, NULL, NULL, 1))
 +              return error("Could not remove config section '%s'", buf.buf);
 +      for (i = 0; i < oldremote->fetch_refspec_nr; i++) {
 +              char *ptr;
 +
 +              strbuf_reset(&buf2);
 +              strbuf_addstr(&buf2, oldremote->fetch_refspec[i]);
 +              ptr = strstr(buf2.buf, rename.old);
 +              if (ptr)
 +                      strbuf_splice(&buf2, ptr-buf2.buf, strlen(rename.old),
 +                                      rename.new, strlen(rename.new));
 +              if (git_config_set_multivar(buf.buf, buf2.buf, "^$", 0))
 +                      return error("Could not append '%s'", buf.buf);
 +      }
 +
 +      read_branches();
 +      for (i = 0; i < branch_list.nr; i++) {
 +              struct string_list_item *item = branch_list.items + i;
 +              struct branch_info *info = item->util;
 +              if (info->remote && !strcmp(info->remote, rename.old)) {
 +                      strbuf_reset(&buf);
 +                      strbuf_addf(&buf, "branch.%s.remote", item->string);
 +                      if (git_config_set(buf.buf, rename.new)) {
 +                              return error("Could not set '%s'", buf.buf);
 +                      }
 +              }
 +      }
 +
 +      /*
 +       * First remove symrefs, then rename the rest, finally create
 +       * the new symrefs.
 +       */
 +      for_each_ref(read_remote_branches, &rename);
 +      for (i = 0; i < remote_branches.nr; i++) {
 +              struct string_list_item *item = remote_branches.items + i;
 +              int flag = 0;
 +              unsigned char sha1[20];
 +              const char *symref;
 +
 +              symref = resolve_ref(item->string, sha1, 1, &flag);
 +              if (!(flag & REF_ISSYMREF))
 +                      continue;
 +              if (delete_ref(item->string, NULL, REF_NODEREF))
 +                      die("deleting '%s' failed", item->string);
 +      }
 +      for (i = 0; i < remote_branches.nr; i++) {
 +              struct string_list_item *item = remote_branches.items + i;
 +
 +              if (item->util)
 +                      continue;
 +              strbuf_reset(&buf);
 +              strbuf_addstr(&buf, item->string);
 +              strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old),
 +                              rename.new, strlen(rename.new));
 +              strbuf_reset(&buf2);
 +              strbuf_addf(&buf2, "remote: renamed %s to %s",
 +                              item->string, buf.buf);
 +              if (rename_ref(item->string, buf.buf, buf2.buf))
 +                      die("renaming '%s' failed", item->string);
 +      }
 +      for (i = 0; i < remote_branches.nr; i++) {
 +              struct string_list_item *item = remote_branches.items + i;
 +
 +              if (!item->util)
 +                      continue;
 +              strbuf_reset(&buf);
 +              strbuf_addstr(&buf, item->string);
 +              strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old),
 +                              rename.new, strlen(rename.new));
 +              strbuf_reset(&buf2);
 +              strbuf_addstr(&buf2, item->util);
 +              strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old),
 +                              rename.new, strlen(rename.new));
 +              strbuf_reset(&buf3);
 +              strbuf_addf(&buf3, "remote: renamed %s to %s",
 +                              item->string, buf.buf);
 +              if (create_symref(buf.buf, buf2.buf, buf3.buf))
 +                      die("creating '%s' failed", buf.buf);
 +      }
 +      return 0;
 +}
 +
  static int remove_branches(struct string_list *branches)
  {
        int i, result = 0;
@@@ -539,11 -362,14 +549,14 @@@ static int rm(int argc, const char **ar
                OPT_END()
        };
        struct remote *remote;
 -      struct strbuf buf;
 +      struct strbuf buf = STRBUF_INIT;
        struct known_remotes known_remotes = { NULL, NULL };
        struct string_list branches = { NULL, 0, 0, 1 };
-       struct branches_for_remote cb_data = { NULL, &branches, &known_remotes };
-       int i;
+       struct string_list skipped = { NULL, 0, 0, 1 };
+       struct branches_for_remote cb_data = {
+               NULL, &branches, &skipped, &known_remotes
+       };
+       int i, result;
  
        if (argc != 2)
                usage_with_options(builtin_remote_usage, options);
        known_remotes.to_delete = remote;
        for_each_remote(add_known_remote, &known_remotes);
  
 -      strbuf_init(&buf, 0);
        strbuf_addf(&buf, "remote.%s", remote->name);
        if (git_config_rename_section(buf.buf, NULL) < 1)
                return error("Could not remove config section '%s'", buf.buf);
         * refs, which are invalidated when deleting a branch.
         */
        cb_data.remote = remote;
-       i = for_each_ref(add_branch_for_removal, &cb_data);
+       result = for_each_ref(add_branch_for_removal, &cb_data);
        strbuf_release(&buf);
  
-       if (!i)
-               i = remove_branches(&branches);
+       if (!result)
+               result = remove_branches(&branches);
        string_list_clear(&branches, 1);
  
-       return i;
+       if (skipped.nr) {
+               fprintf(stderr, skipped.nr == 1 ?
+                       "Note: A non-remote branch was not removed; "
+                       "to delete it, use:\n" :
+                       "Note: Non-remote branches were not removed; "
+                       "to delete them, use:\n");
+               for (i = 0; i < skipped.nr; i++)
+                       fprintf(stderr, "  git branch -d %s\n",
+                               skipped.items[i].string);
+       }
+       string_list_clear(&skipped, 0);
+       return result;
  }
  
  static void show_list(const char *title, struct string_list *list,
                return;
  
        printf(title, list->nr > 1 ? "es" : "", extra_arg);
 -      printf("\n    ");
 -      for (i = 0; i < list->nr; i++)
 -              printf("%s%s", i ? " " : "", list->items[i].string);
        printf("\n");
 +      for (i = 0; i < list->nr; i++)
 +              printf("    %s\n", list->items[i].string);
  }
  
  static int get_remote_ref_states(const char *name,
@@@ -700,17 -540,17 +725,17 @@@ static int show(int argc, const char **
                show_list("  Tracked remote branch%s", &states.tracked, "");
  
                if (states.remote->push_refspec_nr) {
 -                      printf("  Local branch%s pushed with 'git push'\n   ",
 +                      printf("  Local branch%s pushed with 'git push'\n",
                                states.remote->push_refspec_nr > 1 ?
                                        "es" : "");
                        for (i = 0; i < states.remote->push_refspec_nr; i++) {
                                struct refspec *spec = states.remote->push + i;
 -                              printf(" %s%s%s%s", spec->force ? "+" : "",
 +                              printf("    %s%s%s%s\n",
 +                                     spec->force ? "+" : "",
                                       abbrev_branch(spec->src),
                                       spec->dst ? ":" : "",
                                       spec->dst ? abbrev_branch(spec->dst) : "");
                        }
 -                      printf("\n");
                }
  
                /* NEEDSWORK: free remote */
@@@ -774,7 -614,7 +799,7 @@@ static int get_one_remote_for_update(st
  {
        struct string_list *list = priv;
        if (!remote->skip_default_update)
 -              string_list_append(xstrdup(remote->name), list);
 +              string_list_append(remote->name, list);
        return 0;
  }
  
@@@ -885,8 -725,6 +910,8 @@@ int cmd_remote(int argc, const char **a
                result = show_all();
        else if (!strcmp(argv[0], "add"))
                result = add(argc, argv);
 +      else if (!strcmp(argv[0], "rename"))
 +              result = mv(argc, argv);
        else if (!strcmp(argv[0], "rm"))
                result = rm(argc, argv);
        else if (!strcmp(argv[0], "show"))
diff --combined t/t5505-remote.sh
index 1f59960d90c31f02768666c86654a97e1fad9305,aadf3e954620df1bc227ec0145c4fded3090eac5..bc5b7ce4a6b5fd898d1ea5a9f3e6a148d45f0535
@@@ -28,7 -28,7 +28,7 @@@ tokens_match () 
  }
  
  check_remote_track () {
 -      actual=$(git remote show "$1" | sed -n -e '$p') &&
 +      actual=$(git remote show "$1" | sed -e '1,/Tracked/d') &&
        shift &&
        tokens_match "$*" "$actual"
  }
@@@ -107,6 -107,32 +107,32 @@@ test_expect_success 'remove remote' 
  )
  '
  
+ test_expect_success 'remove remote protects non-remote branches' '
+ (
+       cd test &&
+       (cat >expect1 <<EOF
+ Note: A non-remote branch was not removed; to delete it, use:
+   git branch -d master
+ EOF
+     cat >expect2 <<EOF
+ Note: Non-remote branches were not removed; to delete them, use:
+   git branch -d foobranch
+   git branch -d master
+ EOF
+ ) &&
+       git tag footag
+       git config --add remote.oops.fetch "+refs/*:refs/*" &&
+       git remote rm oops 2>actual1 &&
+       git branch foobranch &&
+       git config --add remote.oops.fetch "+refs/*:refs/*" &&
+       git remote rm oops 2>actual2 &&
+       git branch -d foobranch &&
+       git tag -d footag &&
+       test_cmp expect1 actual1 &&
+       test_cmp expect2 actual2
+ )
+ '
  cat > test/expect << EOF
  * remote origin
    URL: $(pwd)/one
    New remote branch (next fetch will store in remotes/origin)
      master
    Tracked remote branches
 -    side master
 +    side
 +    master
    Local branches pushed with 'git push'
 -    master:upstream +refs/tags/lastbackup
 +    master:upstream
 +    +refs/tags/lastbackup
  EOF
  
  test_expect_success 'show' '
@@@ -146,11 -170,9 +172,11 @@@ cat > test/expect << EO
    Remote branch merged with 'git pull' while on branch master
      master
    Tracked remote branches
 -    master side
 +    master
 +    side
    Local branches pushed with 'git push'
 -    master:upstream +refs/tags/lastbackup
 +    master:upstream
 +    +refs/tags/lastbackup
  EOF
  
  test_expect_success 'show -n' '
@@@ -328,52 -350,4 +354,52 @@@ test_expect_success 'reject adding remo
  
  '
  
 +# The first three test if the tracking branches are properly renamed,
 +# the last two ones check if the config is updated.
 +
 +test_expect_success 'rename a remote' '
 +
 +      git clone one four &&
 +      (cd four &&
 +       git remote rename origin upstream &&
 +       rmdir .git/refs/remotes/origin &&
 +       test "$(git symbolic-ref refs/remotes/upstream/HEAD)" = "refs/remotes/upstream/master" &&
 +       test "$(git rev-parse upstream/master)" = "$(git rev-parse master)" &&
 +       test "$(git config remote.upstream.fetch)" = "+refs/heads/*:refs/remotes/upstream/*" &&
 +       test "$(git config branch.master.remote)" = "upstream")
 +
 +'
 +
 +cat > remotes_origin << EOF
 +URL: $(pwd)/one
 +Push: refs/heads/master:refs/heads/upstream
 +Pull: refs/heads/master:refs/heads/origin
 +EOF
 +
 +test_expect_success 'migrate a remote from named file in $GIT_DIR/remotes' '
 +      git clone one five &&
 +      origin_url=$(pwd)/one &&
 +      (cd five &&
 +       git remote rm origin &&
 +       mkdir -p .git/remotes &&
 +       cat ../remotes_origin > .git/remotes/origin &&
 +       git remote rename origin origin &&
 +       ! test -f .git/remotes/origin &&
 +       test "$(git config remote.origin.url)" = "$origin_url" &&
 +       test "$(git config remote.origin.push)" = "refs/heads/master:refs/heads/upstream" &&
 +       test "$(git config remote.origin.fetch)" = "refs/heads/master:refs/heads/origin")
 +'
 +
 +test_expect_success 'migrate a remote from named file in $GIT_DIR/branches' '
 +      git clone one six &&
 +      origin_url=$(pwd)/one &&
 +      (cd six &&
 +       git remote rm origin &&
 +       echo "$origin_url" > .git/branches/origin &&
 +       git remote rename origin origin &&
 +       ! test -f .git/branches/origin &&
 +       test "$(git config remote.origin.url)" = "$origin_url" &&
 +       test "$(git config remote.origin.fetch)" = "refs/heads/master:refs/heads/origin")
 +'
 +
  test_done