Merge branch 'jk/cache-tree-protect-from-broken-libgit2'
[gitweb.git] / builtin / tag.c
index 74d3780b77548fc8a7bb2f51b47a51cbfb51969a..e633f4efdbb8963449fddb5c3357fa657283430e 100644 (file)
@@ -27,9 +27,18 @@ static const char * const git_tag_usage[] = {
        NULL
 };
 
+#define STRCMP_SORT     0      /* must be zero */
+#define VERCMP_SORT     1
+#define SORT_MASK       0x7fff
+#define REVERSE_SORT    0x8000
+
+static int tag_sort;
+
 struct tag_filter {
        const char **patterns;
        int lines;
+       int sort;
+       struct string_list tags;
        struct commit_list *with_commit;
 };
 
@@ -42,7 +51,7 @@ static int match_pattern(const char **patterns, const char *ref)
        if (!*patterns)
                return 1;
        for (; *patterns; patterns++)
-               if (!fnmatch(*patterns, ref, 0))
+               if (!wildmatch(*patterns, ref, 0, NULL))
                        return 1;
        return 0;
 }
@@ -73,11 +82,19 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
        return 0;
 }
 
-static int contains_recurse(struct commit *candidate,
+enum contains_result {
+       CONTAINS_UNKNOWN = -1,
+       CONTAINS_NO = 0,
+       CONTAINS_YES = 1
+};
+
+/*
+ * Test whether the candidate or one of its parents is contained in the list.
+ * Do not recurse to find out, though, but return -1 if inconclusive.
+ */
+static enum contains_result contains_test(struct commit *candidate,
                            const struct commit_list *want)
 {
-       struct commit_list *p;
-
        /* was it previously marked as containing a want commit? */
        if (candidate->object.flags & TMP_MARK)
                return 1;
@@ -85,26 +102,78 @@ static int contains_recurse(struct commit *candidate,
        if (candidate->object.flags & UNINTERESTING)
                return 0;
        /* or are we it? */
-       if (in_commit_list(want, candidate))
+       if (in_commit_list(want, candidate)) {
+               candidate->object.flags |= TMP_MARK;
                return 1;
+       }
 
        if (parse_commit(candidate) < 0)
                return 0;
 
-       /* Otherwise recurse and mark ourselves for future traversals. */
-       for (p = candidate->parents; p; p = p->next) {
-               if (contains_recurse(p->item, want)) {
-                       candidate->object.flags |= TMP_MARK;
-                       return 1;
-               }
-       }
-       candidate->object.flags |= UNINTERESTING;
-       return 0;
+       return -1;
+}
+
+/*
+ * Mimicking the real stack, this stack lives on the heap, avoiding stack
+ * overflows.
+ *
+ * At each recursion step, the stack items points to the commits whose
+ * ancestors are to be inspected.
+ */
+struct stack {
+       int nr, alloc;
+       struct stack_entry {
+               struct commit *commit;
+               struct commit_list *parents;
+       } *stack;
+};
+
+static void push_to_stack(struct commit *candidate, struct stack *stack)
+{
+       int index = stack->nr++;
+       ALLOC_GROW(stack->stack, stack->nr, stack->alloc);
+       stack->stack[index].commit = candidate;
+       stack->stack[index].parents = candidate->parents;
 }
 
-static int contains(struct commit *candidate, const struct commit_list *want)
+static enum contains_result contains(struct commit *candidate,
+               const struct commit_list *want)
 {
-       return contains_recurse(candidate, want);
+       struct stack stack = { 0, 0, NULL };
+       int result = contains_test(candidate, want);
+
+       if (result != CONTAINS_UNKNOWN)
+               return result;
+
+       push_to_stack(candidate, &stack);
+       while (stack.nr) {
+               struct stack_entry *entry = &stack.stack[stack.nr - 1];
+               struct commit *commit = entry->commit;
+               struct commit_list *parents = entry->parents;
+
+               if (!parents) {
+                       commit->object.flags |= UNINTERESTING;
+                       stack.nr--;
+               }
+               /*
+                * If we just popped the stack, parents->item has been marked,
+                * therefore contains_test will return a meaningful 0 or 1.
+                */
+               else switch (contains_test(parents->item, want)) {
+               case CONTAINS_YES:
+                       commit->object.flags |= TMP_MARK;
+                       stack.nr--;
+                       break;
+               case CONTAINS_NO:
+                       entry->parents = parents->next;
+                       break;
+               case CONTAINS_UNKNOWN:
+                       push_to_stack(parents->item, &stack);
+                       break;
+               }
+       }
+       free(stack.stack);
+       return contains_test(candidate, want);
 }
 
 static void show_tag_lines(const unsigned char *sha1, int lines)
@@ -166,7 +235,10 @@ static int show_reference(const char *refname, const unsigned char *sha1,
                        return 0;
 
                if (!filter->lines) {
-                       printf("%s\n", refname);
+                       if (filter->sort)
+                               string_list_append(&filter->tags, refname);
+                       else
+                               printf("%s\n", refname);
                        return 0;
                }
                printf("%-15s ", refname);
@@ -177,17 +249,39 @@ static int show_reference(const char *refname, const unsigned char *sha1,
        return 0;
 }
 
+static int sort_by_version(const void *a_, const void *b_)
+{
+       const struct string_list_item *a = a_;
+       const struct string_list_item *b = b_;
+       return versioncmp(a->string, b->string);
+}
+
 static int list_tags(const char **patterns, int lines,
-                       struct commit_list *with_commit)
+                    struct commit_list *with_commit, int sort)
 {
        struct tag_filter filter;
 
        filter.patterns = patterns;
        filter.lines = lines;
+       filter.sort = sort;
        filter.with_commit = with_commit;
+       memset(&filter.tags, 0, sizeof(filter.tags));
+       filter.tags.strdup_strings = 1;
 
        for_each_tag_ref(show_reference, (void *) &filter);
-
+       if (sort) {
+               int i;
+               if ((sort & SORT_MASK) == VERCMP_SORT)
+                       qsort(filter.tags.items, filter.tags.nr,
+                             sizeof(struct string_list_item), sort_by_version);
+               if (sort & REVERSE_SORT)
+                       for (i = filter.tags.nr - 1; i >= 0; i--)
+                               printf("%s\n", filter.tags.items[i].string);
+               else
+                       for (i = 0; i < filter.tags.nr; i++)
+                               printf("%s\n", filter.tags.items[i].string);
+               string_list_clear(&filter.tags, 0);
+       }
        return 0;
 }
 
@@ -246,17 +340,59 @@ static int do_sign(struct strbuf *buffer)
 }
 
 static const char tag_template[] =
-       N_("\nWrite a tag message\n"
+       N_("\nWrite a message for tag:\n  %s\n"
        "Lines starting with '%c' will be ignored.\n");
 
 static const char tag_template_nocleanup[] =
-       N_("\nWrite a tag message\n"
+       N_("\nWrite a message for tag:\n  %s\n"
        "Lines starting with '%c' will be kept; you may remove them"
        " yourself if you want to.\n");
 
+/*
+ * Parse a sort string, and return 0 if parsed successfully. Will return
+ * non-zero when the sort string does not parse into a known type. If var is
+ * given, the error message becomes a warning and includes information about
+ * the configuration value.
+ */
+static int parse_sort_string(const char *var, const char *arg, int *sort)
+{
+       int type = 0, flags = 0;
+
+       if (skip_prefix(arg, "-", &arg))
+               flags |= REVERSE_SORT;
+
+       if (skip_prefix(arg, "version:", &arg) || skip_prefix(arg, "v:", &arg))
+               type = VERCMP_SORT;
+       else
+               type = STRCMP_SORT;
+
+       if (strcmp(arg, "refname")) {
+               if (!var)
+                       return error(_("unsupported sort specification '%s'"), arg);
+               else {
+                       warning(_("unsupported sort specification '%s' in variable '%s'"),
+                               var, arg);
+                       return -1;
+               }
+       }
+
+       *sort = (type | flags);
+
+       return 0;
+}
+
 static int git_tag_config(const char *var, const char *value, void *cb)
 {
-       int status = git_gpg_config(var, value, cb);
+       int status;
+
+       if (!strcmp(var, "tag.sort")) {
+               if (!value)
+                       return config_error_nonbool(var);
+               parse_sort_string(var, value, &tag_sort);
+               return 0;
+       }
+
+       status = git_gpg_config(var, value, cb);
        if (status)
                return status;
        if (starts_with(var, "column."))
@@ -346,9 +482,9 @@ static void create_tag(const unsigned char *object, const char *tag,
                        struct strbuf buf = STRBUF_INIT;
                        strbuf_addch(&buf, '\n');
                        if (opt->cleanup_mode == CLEANUP_ALL)
-                               strbuf_commented_addf(&buf, _(tag_template), comment_line_char);
+                               strbuf_commented_addf(&buf, _(tag_template), tag, comment_line_char);
                        else
-                               strbuf_commented_addf(&buf, _(tag_template_nocleanup), comment_line_char);
+                               strbuf_commented_addf(&buf, _(tag_template_nocleanup), tag, comment_line_char);
                        write_or_die(fd, buf.buf, buf.len);
                        strbuf_release(&buf);
                }
@@ -427,13 +563,19 @@ static int parse_opt_points_at(const struct option *opt __attribute__((unused)),
        return 0;
 }
 
+static int parse_opt_sort(const struct option *opt, const char *arg, int unset)
+{
+       int *sort = opt->value;
+
+       return parse_sort_string(NULL, arg, sort);
+}
+
 int cmd_tag(int argc, const char **argv, const char *prefix)
 {
        struct strbuf buf = STRBUF_INIT;
        struct strbuf ref = STRBUF_INIT;
        unsigned char object[20], prev[20];
        const char *object_ref, *tag;
-       struct ref_lock *lock;
        struct create_tag_options opt;
        char *cleanup_arg = NULL;
        int annotate = 0, force = 0, lines = -1;
@@ -441,6 +583,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
        const char *msgfile = NULL, *keyid = NULL;
        struct msg_arg msg = { 0, STRBUF_INIT };
        struct commit_list *with_commit = NULL;
+       struct ref_transaction *transaction;
+       struct strbuf err = STRBUF_INIT;
        struct option options[] = {
                OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'),
                { OPTION_INTEGER, 'n', NULL, &lines, N_("n"),
@@ -458,10 +602,14 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
                OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")),
                OPT_STRING(0, "cleanup", &cleanup_arg, N_("mode"),
                        N_("how to strip spaces and #comments from message")),
-               OPT_STRING('u', "local-user", &keyid, N_("key id"),
+               OPT_STRING('u', "local-user", &keyid, N_("key-id"),
                                        N_("use another key to sign the tag")),
                OPT__FORCE(&force, N_("replace the tag if exists")),
                OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
+               {
+                       OPTION_CALLBACK, 0, "sort", &tag_sort, N_("type"), N_("sort tags"),
+                       PARSE_OPT_NONEG, parse_opt_sort
+               },
 
                OPT_GROUP(N_("Tag listing options")),
                {
@@ -470,6 +618,12 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
                        PARSE_OPT_LASTARG_DEFAULT,
                        parse_opt_with_commit, (intptr_t)"HEAD",
                },
+               {
+                       OPTION_CALLBACK, 0, "with", &with_commit, N_("commit"),
+                       N_("print only tags that contain the commit"),
+                       PARSE_OPT_HIDDEN | PARSE_OPT_LASTARG_DEFAULT,
+                       parse_opt_with_commit, (intptr_t)"HEAD",
+               },
                {
                        OPTION_CALLBACK, 0, "points-at", NULL, N_("object"),
                        N_("print only tags of the object"), 0, parse_opt_points_at
@@ -509,7 +663,9 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
                        copts.padding = 2;
                        run_column_filter(colopts, &copts);
                }
-               ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit);
+               if (lines != -1 && tag_sort)
+                       die(_("--sort and -n are incompatible"));
+               ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit, tag_sort);
                if (column_active(colopts))
                        stop_column_filter();
                return ret;
@@ -574,14 +730,17 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
        if (annotate)
                create_tag(object, tag, &buf, &opt, prev, object);
 
-       lock = lock_any_ref_for_update(ref.buf, prev, 0, NULL);
-       if (!lock)
-               die(_("%s: cannot lock the ref"), ref.buf);
-       if (write_ref_sha1(lock, object, NULL) < 0)
-               die(_("%s: cannot update the ref"), ref.buf);
+       transaction = ref_transaction_begin(&err);
+       if (!transaction ||
+           ref_transaction_update(transaction, ref.buf, object, prev,
+                                  0, 1, NULL, &err) ||
+           ref_transaction_commit(transaction, &err))
+               die("%s", err.buf);
+       ref_transaction_free(transaction);
        if (force && !is_null_sha1(prev) && hashcmp(prev, object))
                printf(_("Updated tag '%s' (was %s)\n"), tag, find_unique_abbrev(prev, DEFAULT_ABBREV));
 
+       strbuf_release(&err);
        strbuf_release(&buf);
        strbuf_release(&ref);
        return 0;