Merge branch 'bc/object-id'
[gitweb.git] / transport.c
index 41eb82c6f3f4de956225eb8afd65d0684be68eec..9bfcf870f9078f75349bd3b4976c71bbf5c49ebf 100644 (file)
@@ -59,7 +59,7 @@ static void set_upstreams(struct transport *transport, struct ref *refs,
                                localname + 11, transport->remote->name,
                                remotename);
                else
-                       printf("Would set upstream of '%s' to '%s' of '%s'\n",
+                       printf(_("Would set upstream of '%s' to '%s' of '%s'\n"),
                                localname + 11, remotename + 11,
                                transport->remote->name);
        }
@@ -87,7 +87,7 @@ static struct ref *get_refs_from_bundle(struct transport *transport, int for_pus
        for (i = 0; i < data->header.references.nr; i++) {
                struct ref_list_entry *e = data->header.references.list + i;
                struct ref *ref = alloc_ref(e->name);
-               hashcpy(ref->old_oid.hash, e->sha1);
+               oidcpy(&ref->old_oid, &e->oid);
                ref->next = result;
                result = ref;
        }
@@ -116,8 +116,8 @@ struct git_transport_data {
        struct child_process *conn;
        int fd[2];
        unsigned got_remote_heads : 1;
-       struct sha1_array extra_have;
-       struct sha1_array shallow;
+       struct oid_array extra_have;
+       struct oid_array shallow;
 };
 
 static int set_git_option(struct git_transport_options *opts,
@@ -148,9 +148,18 @@ static int set_git_option(struct git_transport_options *opts,
                        char *end;
                        opts->depth = strtol(value, &end, 0);
                        if (*end)
-                               die("transport: invalid depth option '%s'", value);
+                               die(_("transport: invalid depth option '%s'"), value);
                }
                return 0;
+       } else if (!strcmp(name, TRANS_OPT_DEEPEN_SINCE)) {
+               opts->deepen_since = value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_DEEPEN_NOT)) {
+               opts->deepen_not = (const struct string_list *)value;
+               return 0;
+       } else if (!strcmp(name, TRANS_OPT_DEEPEN_RELATIVE)) {
+               opts->deepen_relative = !!value;
+               return 0;
        }
        return 1;
 }
@@ -195,6 +204,7 @@ static struct ref *get_refs_via_connect(struct transport *transport, int for_pus
 static int fetch_refs_via_pack(struct transport *transport,
                               int nr_heads, struct ref **to_fetch)
 {
+       int ret = 0;
        struct git_transport_data *data = transport->data;
        struct ref *refs;
        char *dest = xstrdup(transport->url);
@@ -211,6 +221,9 @@ static int fetch_refs_via_pack(struct transport *transport,
        args.quiet = (transport->verbose < 0);
        args.no_progress = !transport->progress;
        args.depth = data->options.depth;
+       args.deepen_since = data->options.deepen_since;
+       args.deepen_not = data->options.deepen_not;
+       args.deepen_relative = data->options.deepen_relative;
        args.check_self_contained_and_connected =
                data->options.check_self_contained_and_connected;
        args.cloning = transport->cloning;
@@ -229,19 +242,22 @@ static int fetch_refs_via_pack(struct transport *transport,
                          &transport->pack_lockfile);
        close(data->fd[0]);
        close(data->fd[1]);
-       if (finish_connect(data->conn)) {
-               free_refs(refs);
-               refs = NULL;
-       }
+       if (finish_connect(data->conn))
+               ret = -1;
        data->conn = NULL;
        data->got_remote_heads = 0;
        data->options.self_contained_and_connected =
                args.self_contained_and_connected;
 
+       if (refs == NULL)
+               ret = -1;
+       if (report_unmatched_refs(to_fetch, nr_heads))
+               ret = -1;
+
        free_refs(refs_tmp);
        free_refs(refs);
        free(dest);
-       return (refs ? 0 : -1);
+       return ret;
 }
 
 static int push_had_errors(struct ref *ref)
@@ -287,7 +303,7 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
                if (verbose)
                        fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
                if (ref->deletion) {
-                       delete_ref(rs.dst, NULL, 0);
+                       delete_ref(NULL, rs.dst, NULL, 0);
                } else
                        update_ref("update by push", rs.dst,
                                        ref->new_oid.hash, NULL, 0, 0);
@@ -295,7 +311,9 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
        }
 }
 
-static void print_ref_status(char flag, const char *summary, struct ref *to, struct ref *from, const char *msg, int porcelain)
+static void print_ref_status(char flag, const char *summary,
+                            struct ref *to, struct ref *from, const char *msg,
+                            int porcelain, int summary_width)
 {
        if (porcelain) {
                if (from)
@@ -307,7 +325,7 @@ static void print_ref_status(char flag, const char *summary, struct ref *to, str
                else
                        fprintf(stdout, "%s\n", summary);
        } else {
-               fprintf(stderr, " %c %-*s ", flag, TRANSPORT_SUMMARY_WIDTH, summary);
+               fprintf(stderr, " %c %-*s ", flag, summary_width, summary);
                if (from)
                        fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
                else
@@ -321,15 +339,16 @@ static void print_ref_status(char flag, const char *summary, struct ref *to, str
        }
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain)
+static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
 {
        if (ref->deletion)
-               print_ref_status('-', "[deleted]", ref, NULL, NULL, porcelain);
+               print_ref_status('-', "[deleted]", ref, NULL, NULL,
+                                porcelain, summary_width);
        else if (is_null_oid(&ref->old_oid))
                print_ref_status('*',
                        (starts_with(ref->name, "refs/tags/") ? "[new tag]" :
                        "[new branch]"),
-                       ref, ref->peer_ref, NULL, porcelain);
+                       ref, ref->peer_ref, NULL, porcelain, summary_width);
        else {
                struct strbuf quickref = STRBUF_INIT;
                char type;
@@ -349,12 +368,14 @@ static void print_ok_ref_status(struct ref *ref, int porcelain)
                strbuf_add_unique_abbrev(&quickref, ref->new_oid.hash,
                                         DEFAULT_ABBREV);
 
-               print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg, porcelain);
+               print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
+                                porcelain, summary_width);
                strbuf_release(&quickref);
        }
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count, int porcelain)
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+                                int porcelain, int summary_width)
 {
        if (!count) {
                char *url = transport_anonymize_url(dest);
@@ -364,88 +385,117 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count, i
 
        switch(ref->status) {
        case REF_STATUS_NONE:
-               print_ref_status('X', "[no match]", ref, NULL, NULL, porcelain);
+               print_ref_status('X', "[no match]", ref, NULL, NULL,
+                                porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_NODELETE:
                print_ref_status('!', "[rejected]", ref, NULL,
-                                                "remote does not support deleting refs", porcelain);
+                                "remote does not support deleting refs",
+                                porcelain, summary_width);
                break;
        case REF_STATUS_UPTODATE:
                print_ref_status('=', "[up to date]", ref,
-                                                ref->peer_ref, NULL, porcelain);
+                                ref->peer_ref, NULL, porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_NONFASTFORWARD:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "non-fast-forward", porcelain);
+                                "non-fast-forward", porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_ALREADY_EXISTS:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "already exists", porcelain);
+                                "already exists", porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_FETCH_FIRST:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "fetch first", porcelain);
+                                "fetch first", porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_NEEDS_FORCE:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "needs force", porcelain);
+                                "needs force", porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_STALE:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "stale info", porcelain);
+                                "stale info", porcelain, summary_width);
                break;
        case REF_STATUS_REJECT_SHALLOW:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "new shallow roots not allowed", porcelain);
+                                "new shallow roots not allowed",
+                                porcelain, summary_width);
                break;
        case REF_STATUS_REMOTE_REJECT:
                print_ref_status('!', "[remote rejected]", ref,
-                                                ref->deletion ? NULL : ref->peer_ref,
-                                                ref->remote_status, porcelain);
+                                ref->deletion ? NULL : ref->peer_ref,
+                                ref->remote_status, porcelain, summary_width);
                break;
        case REF_STATUS_EXPECTING_REPORT:
                print_ref_status('!', "[remote failure]", ref,
-                                                ref->deletion ? NULL : ref->peer_ref,
-                                                "remote failed to report status", porcelain);
+                                ref->deletion ? NULL : ref->peer_ref,
+                                "remote failed to report status",
+                                porcelain, summary_width);
                break;
        case REF_STATUS_ATOMIC_PUSH_FAILED:
                print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-                                                "atomic push failed", porcelain);
+                                "atomic push failed", porcelain, summary_width);
                break;
        case REF_STATUS_OK:
-               print_ok_ref_status(ref, porcelain);
+               print_ok_ref_status(ref, porcelain, summary_width);
                break;
        }
 
        return 1;
 }
 
+static int measure_abbrev(const struct object_id *oid, int sofar)
+{
+       char hex[GIT_MAX_HEXSZ + 1];
+       int w = find_unique_abbrev_r(hex, oid->hash, DEFAULT_ABBREV);
+
+       return (w < sofar) ? sofar : w;
+}
+
+int transport_summary_width(const struct ref *refs)
+{
+       int maxw = -1;
+
+       for (; refs; refs = refs->next) {
+               maxw = measure_abbrev(&refs->old_oid, maxw);
+               maxw = measure_abbrev(&refs->new_oid, maxw);
+       }
+       if (maxw < 0)
+               maxw = FALLBACK_DEFAULT_ABBREV;
+       return (2 * maxw + 3);
+}
+
 void transport_print_push_status(const char *dest, struct ref *refs,
                                  int verbose, int porcelain, unsigned int *reject_reasons)
 {
        struct ref *ref;
        int n = 0;
-       unsigned char head_sha1[20];
+       struct object_id head_oid;
        char *head;
+       int summary_width = transport_summary_width(refs);
 
-       head = resolve_refdup("HEAD", RESOLVE_REF_READING, head_sha1, NULL);
+       head = resolve_refdup("HEAD", RESOLVE_REF_READING, head_oid.hash, NULL);
 
        if (verbose) {
                for (ref = refs; ref; ref = ref->next)
                        if (ref->status == REF_STATUS_UPTODATE)
-                               n += print_one_push_status(ref, dest, n, porcelain);
+                               n += print_one_push_status(ref, dest, n,
+                                                          porcelain, summary_width);
        }
 
        for (ref = refs; ref; ref = ref->next)
                if (ref->status == REF_STATUS_OK)
-                       n += print_one_push_status(ref, dest, n, porcelain);
+                       n += print_one_push_status(ref, dest, n,
+                                                  porcelain, summary_width);
 
        *reject_reasons = 0;
        for (ref = refs; ref; ref = ref->next) {
                if (ref->status != REF_STATUS_NONE &&
                    ref->status != REF_STATUS_UPTODATE &&
                    ref->status != REF_STATUS_OK)
-                       n += print_one_push_status(ref, dest, n, porcelain);
+                       n += print_one_push_status(ref, dest, n,
+                                                  porcelain, summary_width);
                if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD) {
                        if (head != NULL && !strcmp(head, ref->name))
                                *reject_reasons |= REJECT_NON_FF_HEAD;
@@ -510,6 +560,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
        args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);
        args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN);
        args.atomic = !!(flags & TRANSPORT_PUSH_ATOMIC);
+       args.push_options = transport->push_options;
        args.url = transport->url;
 
        if (flags & TRANSPORT_PUSH_CERT_ALWAYS)
@@ -563,7 +614,7 @@ void transport_take_over(struct transport *transport,
        struct git_transport_data *data;
 
        if (!transport->smart_options)
-               die("Bug detected: Taking over transport requires non-NULL "
+               die("BUG: taking over transport requires non-NULL "
                    "smart_options field.");
 
        data = xcalloc(1, sizeof(*data));
@@ -617,21 +668,89 @@ static const struct string_list *protocol_whitelist(void)
        return enabled ? &allowed : NULL;
 }
 
-int is_transport_allowed(const char *type)
+enum protocol_allow_config {
+       PROTOCOL_ALLOW_NEVER = 0,
+       PROTOCOL_ALLOW_USER_ONLY,
+       PROTOCOL_ALLOW_ALWAYS
+};
+
+static enum protocol_allow_config parse_protocol_config(const char *key,
+                                                       const char *value)
 {
-       const struct string_list *allowed = protocol_whitelist();
-       return !allowed || string_list_has_string(allowed, type);
+       if (!strcasecmp(value, "always"))
+               return PROTOCOL_ALLOW_ALWAYS;
+       else if (!strcasecmp(value, "never"))
+               return PROTOCOL_ALLOW_NEVER;
+       else if (!strcasecmp(value, "user"))
+               return PROTOCOL_ALLOW_USER_ONLY;
+
+       die("unknown value for config '%s': %s", key, value);
 }
 
-void transport_check_allowed(const char *type)
+static enum protocol_allow_config get_protocol_config(const char *type)
 {
-       if (!is_transport_allowed(type))
-               die("transport '%s' not allowed", type);
+       char *key = xstrfmt("protocol.%s.allow", type);
+       char *value;
+
+       /* first check the per-protocol config */
+       if (!git_config_get_string(key, &value)) {
+               enum protocol_allow_config ret =
+                       parse_protocol_config(key, value);
+               free(key);
+               free(value);
+               return ret;
+       }
+       free(key);
+
+       /* if defined, fallback to user-defined default for unknown protocols */
+       if (!git_config_get_string("protocol.allow", &value)) {
+               enum protocol_allow_config ret =
+                       parse_protocol_config("protocol.allow", value);
+               free(value);
+               return ret;
+       }
+
+       /* fallback to built-in defaults */
+       /* known safe */
+       if (!strcmp(type, "http") ||
+           !strcmp(type, "https") ||
+           !strcmp(type, "git") ||
+           !strcmp(type, "ssh") ||
+           !strcmp(type, "file"))
+               return PROTOCOL_ALLOW_ALWAYS;
+
+       /* known scary; err on the side of caution */
+       if (!strcmp(type, "ext"))
+               return PROTOCOL_ALLOW_NEVER;
+
+       /* unknown; by default let them be used only directly by the user */
+       return PROTOCOL_ALLOW_USER_ONLY;
+}
+
+int is_transport_allowed(const char *type, int from_user)
+{
+       const struct string_list *whitelist = protocol_whitelist();
+       if (whitelist)
+               return string_list_has_string(whitelist, type);
+
+       switch (get_protocol_config(type)) {
+       case PROTOCOL_ALLOW_ALWAYS:
+               return 1;
+       case PROTOCOL_ALLOW_NEVER:
+               return 0;
+       case PROTOCOL_ALLOW_USER_ONLY:
+               if (from_user < 0)
+                       from_user = git_env_bool("GIT_PROTOCOL_FROM_USER", 1);
+               return from_user;
+       }
+
+       die("BUG: invalid protocol_allow_config type");
 }
 
-int transport_restrict_protocols(void)
+void transport_check_allowed(const char *type)
 {
-       return !!protocol_whitelist();
+       if (!is_transport_allowed(type, -1))
+               die("transport '%s' not allowed", type);
 }
 
 struct transport *transport_get(struct remote *remote, const char *url)
@@ -767,19 +886,19 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
 {
        int i;
 
-       fprintf(stderr, "The following submodule paths contain changes that can\n"
-                       "not be found on any remote:\n");
+       fprintf(stderr, _("The following submodule paths contain changes that can\n"
+                       "not be found on any remote:\n"));
        for (i = 0; i < needs_pushing->nr; i++)
-               printf("  %s\n", needs_pushing->items[i].string);
-       fprintf(stderr, "\nPlease try\n\n"
-                       "       git push --recurse-submodules=on-demand\n\n"
-                       "or cd to the path and use\n\n"
-                       "       git push\n\n"
-                       "to push them to a remote.\n\n");
+               fprintf(stderr, "  %s\n", needs_pushing->items[i].string);
+       fprintf(stderr, _("\nPlease try\n\n"
+                         "     git push --recurse-submodules=on-demand\n\n"
+                         "or cd to the path and use\n\n"
+                         "     git push\n\n"
+                         "to push them to a remote.\n\n"));
 
        string_list_clear(needs_pushing, 0);
 
-       die("Aborting.");
+       die(_("Aborting."));
 }
 
 static int run_pre_push_hook(struct transport *transport,
@@ -900,28 +1019,54 @@ int transport_push(struct transport *transport,
                        if (run_pre_push_hook(transport, remote_refs))
                                return -1;
 
-               if ((flags & TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND) && !is_bare_repository()) {
+               if ((flags & (TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND |
+                             TRANSPORT_RECURSE_SUBMODULES_ONLY)) &&
+                   !is_bare_repository()) {
                        struct ref *ref = remote_refs;
+                       struct oid_array commits = OID_ARRAY_INIT;
+
                        for (; ref; ref = ref->next)
-                               if (!is_null_oid(&ref->new_oid) &&
-                                   !push_unpushed_submodules(ref->new_oid.hash,
-                                           transport->remote->name))
-                                   die ("Failed to push all needed submodules!");
+                               if (!is_null_oid(&ref->new_oid))
+                                       oid_array_append(&commits,
+                                                         &ref->new_oid);
+
+                       if (!push_unpushed_submodules(&commits,
+                                                     transport->remote,
+                                                     refspec, refspec_nr,
+                                                     transport->push_options,
+                                                     pretend)) {
+                               oid_array_clear(&commits);
+                               die("Failed to push all needed submodules!");
+                       }
+                       oid_array_clear(&commits);
                }
 
-               if ((flags & (TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND |
-                             TRANSPORT_RECURSE_SUBMODULES_CHECK)) && !is_bare_repository()) {
+               if (((flags & TRANSPORT_RECURSE_SUBMODULES_CHECK) ||
+                    ((flags & (TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND |
+                               TRANSPORT_RECURSE_SUBMODULES_ONLY)) &&
+                     !pretend)) && !is_bare_repository()) {
                        struct ref *ref = remote_refs;
                        struct string_list needs_pushing = STRING_LIST_INIT_DUP;
+                       struct oid_array commits = OID_ARRAY_INIT;
 
                        for (; ref; ref = ref->next)
-                               if (!is_null_oid(&ref->new_oid) &&
-                                   find_unpushed_submodules(ref->new_oid.hash,
-                                           transport->remote->name, &needs_pushing))
-                                       die_with_unpushed_submodules(&needs_pushing);
+                               if (!is_null_oid(&ref->new_oid))
+                                       oid_array_append(&commits,
+                                                         &ref->new_oid);
+
+                       if (find_unpushed_submodules(&commits, transport->remote->name,
+                                               &needs_pushing)) {
+                               oid_array_clear(&commits);
+                               die_with_unpushed_submodules(&needs_pushing);
+                       }
+                       string_list_clear(&needs_pushing, 0);
+                       oid_array_clear(&commits);
                }
 
-               push_ret = transport->push_refs(transport, remote_refs, flags);
+               if (!(flags & TRANSPORT_RECURSE_SUBMODULES_ONLY))
+                       push_ret = transport->push_refs(transport, remote_refs, flags);
+               else
+                       push_ret = 0;
                err = push_had_errors(remote_refs);
                ret = push_ret | err;
 
@@ -933,7 +1078,8 @@ int transport_push(struct transport *transport,
                if (flags & TRANSPORT_PUSH_SET_UPSTREAM)
                        set_upstreams(transport, remote_refs, pretend);
 
-               if (!(flags & TRANSPORT_PUSH_DRY_RUN)) {
+               if (!(flags & (TRANSPORT_PUSH_DRY_RUN |
+                              TRANSPORT_RECURSE_SUBMODULES_ONLY))) {
                        struct ref *ref;
                        for (ref = remote_refs; ref; ref = ref->next)
                                transport_update_tracking_ref(transport->remote, ref, verbose);
@@ -1068,6 +1214,42 @@ char *transport_anonymize_url(const char *url)
        return xstrdup(url);
 }
 
+static void read_alternate_refs(const char *path,
+                               alternate_ref_fn *cb,
+                               void *data)
+{
+       struct child_process cmd = CHILD_PROCESS_INIT;
+       struct strbuf line = STRBUF_INIT;
+       FILE *fh;
+
+       cmd.git_cmd = 1;
+       argv_array_pushf(&cmd.args, "--git-dir=%s", path);
+       argv_array_push(&cmd.args, "for-each-ref");
+       argv_array_push(&cmd.args, "--format=%(objectname) %(refname)");
+       cmd.env = local_repo_env;
+       cmd.out = -1;
+
+       if (start_command(&cmd))
+               return;
+
+       fh = xfdopen(cmd.out, "r");
+       while (strbuf_getline_lf(&line, fh) != EOF) {
+               struct object_id oid;
+
+               if (get_oid_hex(line.buf, &oid) ||
+                   line.buf[GIT_SHA1_HEXSZ] != ' ') {
+                       warning("invalid line while parsing alternate refs: %s",
+                               line.buf);
+                       break;
+               }
+
+               cb(line.buf + GIT_SHA1_HEXSZ + 1, &oid, data);
+       }
+
+       fclose(fh);
+       finish_command(&cmd);
+}
+
 struct alternate_refs_data {
        alternate_ref_fn *fn;
        void *data;
@@ -1076,36 +1258,26 @@ struct alternate_refs_data {
 static int refs_from_alternate_cb(struct alternate_object_database *e,
                                  void *data)
 {
-       char *other;
-       size_t len;
-       struct remote *remote;
-       struct transport *transport;
-       const struct ref *extra;
+       struct strbuf path = STRBUF_INIT;
+       size_t base_len;
        struct alternate_refs_data *cb = data;
 
-       e->name[-1] = '\0';
-       other = xstrdup(real_path(e->base));
-       e->name[-1] = '/';
-       len = strlen(other);
-
-       while (other[len-1] == '/')
-               other[--len] = '\0';
-       if (len < 8 || memcmp(other + len - 8, "/objects", 8))
+       if (!strbuf_realpath(&path, e->path, 0))
+               goto out;
+       if (!strbuf_strip_suffix(&path, "/objects"))
                goto out;
+       base_len = path.len;
+
        /* Is this a git repository with refs? */
-       memcpy(other + len - 8, "/refs", 6);
-       if (!is_directory(other))
+       strbuf_addstr(&path, "/refs");
+       if (!is_directory(path.buf))
                goto out;
-       other[len - 8] = '\0';
-       remote = remote_get(other);
-       transport = transport_get(remote, other);
-       for (extra = transport_get_remote_refs(transport);
-            extra;
-            extra = extra->next)
-               cb->fn(extra, cb->data);
-       transport_disconnect(transport);
+       strbuf_setlen(&path, base_len);
+
+       read_alternate_refs(path.buf, cb->fn, cb->data);
+
 out:
-       free(other);
+       strbuf_release(&path);
        return 0;
 }