summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornulltoken <emeric.fermas@gmail.com>2012-11-13 16:35:24 +0100
committerCarlos Martín Nieto <cmn@dwim.me>2014-04-30 09:46:25 +0200
commit3a728fb508ea3eea8033a9e338c61a6421ad21b2 (patch)
tree6f32280311850282e684840cbcc6c52b5a7c77bd
parent4cc71bb7fb7d10347bfa8ba57cb4cf74dcc79d4b (diff)
downloadlibgit2-3a728fb508ea3eea8033a9e338c61a6421ad21b2.tar.gz
object: introduce git_describe_object()
-rw-r--r--include/git2.h1
-rw-r--r--include/git2/common.h2
-rw-r--r--include/git2/describe.h66
-rw-r--r--include/git2/errors.h1
-rw-r--r--src/commit_list.h2
-rw-r--r--src/describe.c690
-rw-r--r--src/oidmap.h3
-rw-r--r--tests/describe/describe.c47
-rw-r--r--tests/describe/describe_helpers.c24
-rw-r--r--tests/describe/describe_helpers.h9
-rw-r--r--tests/describe/t6120.c135
11 files changed, 979 insertions, 1 deletions
diff --git a/include/git2.h b/include/git2.h
index f74976061..6713b4961 100644
--- a/include/git2.h
+++ b/include/git2.h
@@ -19,6 +19,7 @@
#include "git2/commit.h"
#include "git2/common.h"
#include "git2/config.h"
+#include "git2/describe.h"
#include "git2/diff.h"
#include "git2/errors.h"
#include "git2/filter.h"
diff --git a/include/git2/common.h b/include/git2/common.h
index 32237efed..ceb27205a 100644
--- a/include/git2/common.h
+++ b/include/git2/common.h
@@ -83,6 +83,8 @@ GIT_BEGIN_DECL
*/
#define GIT_OID_HEX_ZERO "0000000000000000000000000000000000000000"
+#define FLAG_BITS 27
+
/**
* Return the version of the libgit2 library
* being currently used.
diff --git a/include/git2/describe.h b/include/git2/describe.h
new file mode 100644
index 000000000..8a00d258a
--- /dev/null
+++ b/include/git2/describe.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_git_describe_h__
+#define INCLUDE_git_describe_h__
+
+#include "common.h"
+#include "types.h"
+#include "buffer.h"
+
+/**
+ * @file git2/describe.h
+ * @brief Git describing routines
+ * @defgroup git_describe Git describing routines
+ * @ingroup Git
+ * @{
+ */
+GIT_BEGIN_DECL
+
+typedef enum {
+ GIT_DESCRIBE_DEFAULT,
+ GIT_DESCRIBE_TAGS,
+ GIT_DESCRIBE_ALL,
+} git_describe_strategy_t;
+
+/**
+ * Describe options structure
+ *
+ * Zero out for defaults. Initialize with `GIT_DESCRIBE_OPTIONS_INIT` macro to
+ * correctly set the `version` field. E.g.
+ *
+ * git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ */
+typedef struct git_describe_opts {
+ unsigned int version;
+
+ unsigned int max_candidates_tags; /** default: 10 */
+ unsigned int abbreviated_size;
+ unsigned int describe_strategy; /** default: GIT_DESCRIBE_DEFAULT */
+ const char *pattern;
+ int always_use_long_format;
+ int only_follow_first_parent;
+ int show_commit_oid_as_fallback;
+} git_describe_opts;
+
+#define GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS 10
+#define GIT_DESCRIBE_DEFAULT_ABBREVIATED_SIZE 7
+
+#define GIT_DESCRIBE_OPTIONS_VERSION 1
+#define GIT_DESCRIBE_OPTIONS_INIT { \
+ GIT_DESCRIBE_OPTIONS_VERSION, \
+ GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS, \
+ GIT_DESCRIBE_DEFAULT_ABBREVIATED_SIZE}
+
+GIT_EXTERN(int) git_describe_object(
+ git_buf *out,
+ git_object *committish,
+ git_describe_opts *opts);
+
+/** @} */
+GIT_END_DECL
+
+#endif
diff --git a/include/git2/errors.h b/include/git2/errors.h
index e22f0d86d..287498423 100644
--- a/include/git2/errors.h
+++ b/include/git2/errors.h
@@ -87,6 +87,7 @@ typedef enum {
GITERR_REVERT,
GITERR_CALLBACK,
GITERR_CHERRYPICK,
+ GITERR_DESCRIBE,
} git_error_t;
/**
diff --git a/src/commit_list.h b/src/commit_list.h
index 490d841be..7cd3945ae 100644
--- a/src/commit_list.h
+++ b/src/commit_list.h
@@ -25,7 +25,7 @@ typedef struct git_commit_list_node {
uninteresting:1,
topo_delay:1,
parsed:1,
- flags : 4;
+ flags : FLAG_BITS;
unsigned short in_degree;
unsigned short out_degree;
diff --git a/src/describe.c b/src/describe.c
new file mode 100644
index 000000000..b503db761
--- /dev/null
+++ b/src/describe.c
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#include "git2/describe.h"
+
+#include "common.h"
+#include "commit.h"
+#include "commit_list.h"
+#include "oidmap.h"
+#include "refs.h"
+#include "revwalk.h"
+#include "tag.h"
+#include "vector.h"
+
+/* Ported from https://github.com/git/git/blob/89dde7882f71f846ccd0359756d27bebc31108de/builtin/describe.c */
+
+struct commit_name {
+ git_tag *tag;
+ unsigned prio:2; /* annotated tag = 2, tag = 1, head = 0 */
+ unsigned name_checked:1;
+ git_oid sha1;
+ char *path;
+
+ /* Khash workaround. They original key has to still be reachable */
+ git_oid peeled;
+};
+
+static void *oidmap_value_bykey(git_oidmap *map, const git_oid *key)
+{
+ khint_t pos = git_oidmap_lookup_index(map, key);
+
+ if (!git_oidmap_valid_index(map, pos))
+ return NULL;
+
+ return git_oidmap_value_at(map, pos);
+}
+
+static struct commit_name *find_commit_name(
+ git_oidmap *names,
+ const git_oid *peeled)
+{
+ return (struct commit_name *)(oidmap_value_bykey(names, peeled));
+}
+
+static int replace_name(
+ git_tag **tag,
+ git_repository *repo,
+ struct commit_name *e,
+ unsigned int prio,
+ const git_oid *sha1)
+{
+ git_time_t e_time = 0, t_time = 0;
+
+ if (!e || e->prio < prio)
+ return 1;
+
+ if (e->prio == 2 && prio == 2) {
+ /* Multiple annotated tags point to the same commit.
+ * Select one to keep based upon their tagger date.
+ */
+ git_tag *t = NULL;
+
+ if (!e->tag) {
+ if (git_tag_lookup(&t, repo, &e->sha1) < 0)
+ return 1;
+ e->tag = t;
+ }
+
+ if (git_tag_lookup(&t, repo, sha1) < 0)
+ return 0;
+
+ *tag = t;
+
+ if (e->tag->tagger)
+ e_time = e->tag->tagger->when.time;
+
+ if (t->tagger)
+ t_time = t->tagger->when.time;
+
+ if (e_time < t_time)
+ return 1;
+ }
+
+ return 0;
+}
+
+static int add_to_known_names(
+ git_repository *repo,
+ git_oidmap *names,
+ const char *path,
+ const git_oid *peeled,
+ unsigned int prio,
+ const git_oid *sha1)
+{
+ struct commit_name *e = find_commit_name(names, peeled);
+ bool found = (e != NULL);
+
+ git_tag *tag = NULL;
+ if (replace_name(&tag, repo, e, prio, sha1)) {
+ if (!found) {
+ e = git__malloc(sizeof(struct commit_name));
+ GITERR_CHECK_ALLOC(e);
+
+ e->path = NULL;
+ e->tag = NULL;
+ }
+
+ if (e->tag)
+ git_tag_free(e->tag);
+ e->tag = tag;
+ e->prio = prio;
+ e->name_checked = 0;
+ git_oid_cpy(&e->sha1, sha1);
+ git__free(e->path);
+ e->path = git__strdup(path);
+ git_oid_cpy(&e->peeled, peeled);
+
+ if (!found) {
+ int ret;
+
+ git_oidmap_insert(names, &e->peeled, e, ret);
+ if (ret < 0)
+ return -1;
+ }
+ }
+ else
+ git_tag_free(tag);
+
+ return 0;
+}
+
+static int retrieve_peeled_tag_or_object_oid(
+ git_oid *peeled_out,
+ git_oid *ref_target_out,
+ git_repository *repo,
+ const char *refname)
+{
+ git_reference *ref;
+ git_object *peeled = NULL;
+ int error;
+
+ if ((error = git_reference_lookup_resolved(&ref, repo, refname, -1)) < 0)
+ return error;
+
+ if ((error = git_reference_peel(&peeled, ref, GIT_OBJ_ANY)) < 0)
+ goto cleanup;
+
+ git_oid_cpy(ref_target_out, git_reference_target(ref));
+ git_oid_cpy(peeled_out, git_object_id(peeled));
+
+ if (git_oid_cmp(ref_target_out, peeled_out) != 0)
+ error = 1; /* The reference was pointing to a annotated tag */
+ else
+ error = 0; /* Any other object */
+
+cleanup:
+ git_reference_free(ref);
+ git_object_free(peeled);
+ return error;
+}
+
+struct get_name_data
+{
+ git_describe_opts *opts;
+ git_repository *repo;
+ git_oidmap *names;
+};
+
+static int get_name(const char *refname, void *payload)
+{
+ struct get_name_data *data;
+ bool is_tag, is_annotated, all;
+ git_oid peeled, sha1;
+ unsigned int prio;
+ int error = 0;
+
+ data = (struct get_name_data *)payload;
+ is_tag = !git__prefixcmp(refname, GIT_REFS_TAGS_DIR);
+ all = data->opts->describe_strategy == GIT_DESCRIBE_ALL;
+
+ /* Reject anything outside refs/tags/ unless --all */
+ if (!all && !is_tag)
+ return 0;
+
+ /* Accept only tags that match the pattern, if given */
+ if (data->opts->pattern && (!is_tag || p_fnmatch(data->opts->pattern,
+ refname + strlen(GIT_REFS_TAGS_DIR), 0)))
+ return 0;
+
+ /* Is it annotated? */
+ if ((error = retrieve_peeled_tag_or_object_oid(
+ &peeled, &sha1, data->repo, refname)) < 0)
+ return error;
+
+ is_annotated = error;
+
+ /*
+ * By default, we only use annotated tags, but with --tags
+ * we fall back to lightweight ones (even without --tags,
+ * we still remember lightweight ones, only to give hints
+ * in an error message). --all allows any refs to be used.
+ */
+ if (is_annotated)
+ prio = 2;
+ else if (is_tag)
+ prio = 1;
+ else
+ prio = 0;
+
+ add_to_known_names(data->repo, data->names,
+ all ? refname + strlen(GIT_REFS_DIR) : refname + strlen(GIT_REFS_TAGS_DIR),
+ &peeled, prio, &sha1);
+ return 0;
+}
+
+struct possible_tag {
+ struct commit_name *name;
+ int depth;
+ int found_order;
+ unsigned flag_within;
+};
+
+static int compare_pt(const void *a_, const void *b_)
+{
+ struct possible_tag *a = (struct possible_tag *)a_;
+ struct possible_tag *b = (struct possible_tag *)b_;
+ if (a->depth != b->depth)
+ return a->depth - b->depth;
+ if (a->found_order != b->found_order)
+ return a->found_order - b->found_order;
+ return 0;
+}
+
+#define SEEN (1u << 0)
+
+static unsigned long finish_depth_computation(
+ git_pqueue *list,
+ git_revwalk *walk,
+ struct possible_tag *best)
+{
+ unsigned long seen_commits = 0;
+ int error, i;
+
+ while (git_pqueue_size(list) > 0) {
+ git_commit_list_node *c = git_pqueue_pop(list);
+ seen_commits++;
+ if (c->flags & best->flag_within) {
+ size_t index = 0;
+ while (git_pqueue_size(list) > index) {
+ git_commit_list_node *i = git_pqueue_get(list, index);
+ if (!(i->flags & best->flag_within))
+ break;
+ index++;
+ }
+ if (index > git_pqueue_size(list))
+ break;
+ } else
+ best->depth++;
+ for (i = 0; i < c->out_degree; i++) {
+ git_commit_list_node *p = c->parents[i];
+ if ((error = git_commit_list_parse(walk, p)) < 0)
+ return error;
+ if (!(p->flags & SEEN))
+ if ((error = git_pqueue_insert(list, p)) < 0)
+ return error;
+ p->flags |= c->flags;
+ }
+ }
+ return seen_commits;
+}
+
+static int display_name(git_buf *buf, git_repository *repo, struct commit_name *n)
+{
+ if (n->prio == 2 && !n->tag) {
+ if (git_tag_lookup(&n->tag, repo, &n->sha1) < 0) {
+ giterr_set(GITERR_TAG, "Annotated tag '%s' not available", n->path);
+ return -1;
+ }
+ }
+
+ if (n->tag && !n->name_checked) {
+ if (!git_tag_name(n->tag)) {
+ giterr_set(GITERR_TAG, "Annotated tag '%s' has no embedded name", n->path);
+ return -1;
+ }
+
+ /* TODO: Cope with warnings
+ if (strcmp(n->tag->tag, all ? n->path + 5 : n->path))
+ warning(_("tag '%s' is really '%s' here"), n->tag->tag, n->path);
+ */
+
+ n->name_checked = 1;
+ }
+
+ if (n->tag)
+ git_buf_printf(buf, "%s", git_tag_name(n->tag));
+ else
+ git_buf_printf(buf, "%s", n->path);
+
+ return 0;
+}
+
+static int find_unique_abbrev_size(
+ int *out,
+ const git_oid *oid_in,
+ int abbreviated_size)
+{
+ GIT_UNUSED(abbreviated_size);
+
+ /* TODO: Actually find the abbreviated oid size */
+ GIT_UNUSED(oid_in);
+
+ *out = GIT_OID_HEXSZ;
+
+ return 0;
+}
+
+static int show_suffix(
+ git_buf *buf,
+ int depth,
+ const git_oid* id,
+ size_t abbrev_size)
+{
+ int error, size;
+
+ char hex_oid[GIT_OID_HEXSZ];
+
+ if ((error = find_unique_abbrev_size(&size, id, abbrev_size)) < 0)
+ return error;
+
+ git_oid_fmt(hex_oid, id);
+
+ git_buf_printf(buf, "-%d-g", depth);
+
+ git_buf_put(buf, hex_oid, size);
+ git_buf_putc(buf, '\0');
+
+ return git_buf_oom(buf) ? -1 : 0;
+}
+
+#define MAX_CANDIDATES_TAGS FLAG_BITS - 1
+
+static int describe_not_found(const git_oid *oid, const char *message_format) {
+ char oid_str[GIT_OID_HEXSZ + 1];
+ git_oid_tostr(oid_str, sizeof(oid_str), oid);
+
+ giterr_set(GITERR_DESCRIBE, message_format, oid_str);
+ return GIT_ENOTFOUND;
+}
+
+static int describe(
+ git_buf *out,
+ struct get_name_data *data,
+ git_commit *commit,
+ const char *dirty_suffix)
+{
+ struct commit_name *n;
+ struct possible_tag *best;
+ bool all, tags;
+ git_buf buf = GIT_BUF_INIT;
+ git_revwalk *walk = NULL;
+ git_pqueue list;
+ git_commit_list_node *cmit, *gave_up_on = NULL;
+ git_vector all_matches = GIT_VECTOR_INIT;
+ unsigned int match_cnt = 0, annotated_cnt = 0, cur_match;
+ unsigned long seen_commits = 0; /* TODO: Check long */
+ unsigned int unannotated_cnt = 0;
+ int error;
+
+ if (git_vector_init(&all_matches, MAX_CANDIDATES_TAGS, compare_pt) < 0)
+ return -1;
+
+ if ((error = git_pqueue_init(&list, 0, 2, git_commit_list_time_cmp)) < 0)
+ goto cleanup;
+
+ all = data->opts->describe_strategy == GIT_DESCRIBE_ALL;
+ tags = data->opts->describe_strategy == GIT_DESCRIBE_TAGS;
+
+ n = find_commit_name(data->names, git_commit_id(commit));
+ if (n && (tags || all || n->prio == 2)) {
+ /*
+ * Exact match to an existing ref.
+ */
+ if ((error = display_name(&buf, data->repo, n)) < 0)
+ goto cleanup;
+
+ if (data->opts->always_use_long_format) {
+ if ((error = show_suffix(&buf, 0,
+ n->tag ? git_tag_target_id(n->tag) : git_commit_id(commit),
+ data->opts->abbreviated_size)) < 0)
+ goto cleanup;
+ }
+
+ if (dirty_suffix)
+ git_buf_printf(&buf, "%s", dirty_suffix);
+
+ if (git_buf_oom(&buf))
+ return -1;
+
+ goto found;
+ }
+
+ if (!data->opts->max_candidates_tags) {
+ error = describe_not_found(
+ git_commit_id(commit),
+ "Cannot describe - no tag exactly matches '%s'");
+
+ goto cleanup;
+ }
+
+ if ((error = git_revwalk_new(&walk, git_commit_owner(commit))) < 0)
+ goto cleanup;
+
+ if ((cmit = git_revwalk__commit_lookup(walk, git_commit_id(commit))) == NULL)
+ goto cleanup;
+
+ if ((error = git_commit_list_parse(walk, cmit)) < 0)
+ goto cleanup;
+
+ cmit->flags = SEEN;
+
+ if ((error = git_pqueue_insert(&list, cmit)) < 0)
+ goto cleanup;
+
+ while (git_pqueue_size(&list) > 0)
+ {
+ int i;
+
+ git_commit_list_node *c = (git_commit_list_node *)git_pqueue_pop(&list);
+ seen_commits++;
+
+ n = find_commit_name(data->names, &c->oid);
+
+ if (n) {
+ if (!tags && !all && n->prio < 2) {
+ unannotated_cnt++;
+ } else if (match_cnt < data->opts->max_candidates_tags) {
+ struct possible_tag *t = git__malloc(sizeof(struct commit_name));
+ GITERR_CHECK_ALLOC(t);
+ if ((error = git_vector_insert(&all_matches, t)) < 0)
+ goto cleanup;
+
+ match_cnt++;
+
+ t->name = n;
+ t->depth = seen_commits - 1;
+ t->flag_within = 1u << match_cnt;
+ t->found_order = match_cnt;
+ c->flags |= t->flag_within;
+ if (n->prio == 2)
+ annotated_cnt++;
+ }
+ else {
+ gave_up_on = c;
+ break;
+ }
+ }
+
+ for (cur_match = 0; cur_match < match_cnt; cur_match++) {
+ struct possible_tag *t = git_vector_get(&all_matches, cur_match);
+ if (!(c->flags & t->flag_within))
+ t->depth++;
+ }
+
+ if (annotated_cnt && (git_pqueue_size(&list) == 0)) {
+ /*
+ if (debug) {
+ char oid_str[GIT_OID_HEXSZ + 1];
+ git_oid_tostr(oid_str, sizeof(oid_str), &c->oid);
+
+ fprintf(stderr, "finished search at %s\n", oid_str);
+ }
+ */
+ break;
+ }
+ for (i = 0; i < c->out_degree; i++) {
+ git_commit_list_node *p = c->parents[i];
+ if ((error = git_commit_list_parse(walk, p)) < 0)
+ goto cleanup;
+ if (!(p->flags & SEEN))
+ if ((error = git_pqueue_insert(&list, p)) < 0)
+ goto cleanup;
+ p->flags |= c->flags;
+
+ if (data->opts->only_follow_first_parent)
+ break;
+ }
+ }
+
+ if (!match_cnt) {
+ if (data->opts->show_commit_oid_as_fallback) {
+ char hex_oid[GIT_OID_HEXSZ];
+ int size;
+
+ if ((error = find_unique_abbrev_size(
+ &size, &cmit->oid, data->opts->abbreviated_size)) < 0)
+ goto cleanup;
+
+ git_oid_fmt(hex_oid, &cmit->oid);
+ git_buf_put(&buf, hex_oid, size);
+
+ if (dirty_suffix)
+ git_buf_printf(&buf, "%s", dirty_suffix);
+
+ if (git_buf_oom(&buf))
+ return -1;
+
+ goto found;
+ }
+ if (unannotated_cnt) {
+ error = describe_not_found(git_commit_id(commit),
+ "Cannot describe - "
+ "No annotated tags can describe '%s'."
+ "However, there were unannotated tags.");
+ goto cleanup;
+ }
+ else {
+ error = describe_not_found(git_commit_id(commit),
+ "Cannot describe - "
+ "No tags can describe '%s'.");
+ goto cleanup;
+ }
+ }
+
+ best = (struct possible_tag *)git_vector_get(&all_matches, 0);
+
+ git_vector_sort(&all_matches);
+
+ best = (struct possible_tag *)git_vector_get(&all_matches, 0);
+
+ if (gave_up_on) {
+ git_pqueue_insert(&list, gave_up_on);
+ seen_commits--;
+ }
+ if ((error = finish_depth_computation(
+ &list, walk, best)) < 0)
+ goto cleanup;
+ seen_commits += error;
+
+ /*
+ {
+ static const char *prio_names[] = {
+ "head", "lightweight", "annotated",
+ };
+
+ char oid_str[GIT_OID_HEXSZ + 1];
+
+ if (debug) {
+ for (cur_match = 0; cur_match < match_cnt; cur_match++) {
+ struct possible_tag *t = (struct possible_tag *)git_vector_get(&all_matches, cur_match);
+ fprintf(stderr, " %-11s %8d %s\n",
+ prio_names[t->name->prio],
+ t->depth, t->name->path);
+ }
+ fprintf(stderr, "traversed %lu commits\n", seen_commits);
+ if (gave_up_on) {
+ git_oid_tostr(oid_str, sizeof(oid_str), &gave_up_on->oid);
+ fprintf(stderr,
+ "more than %i tags found; listed %i most recent\n"
+ "gave up search at %s\n",
+ data->opts->max_candidates_tags, data->opts->max_candidates_tags,
+ oid_str);
+ }
+ }
+ }
+ */
+
+ if ((error = display_name(&buf, data->repo, best->name)) < 0)
+ goto cleanup;
+
+ if (data->opts->abbreviated_size) {
+ if ((error = show_suffix(&buf, best->depth,
+ &cmit->oid, data->opts->abbreviated_size)) < 0)
+ goto cleanup;
+ }
+
+ if (dirty_suffix)
+ git_buf_printf(&buf, "%s", dirty_suffix);
+
+ if (git_buf_oom(&buf))
+ return -1;
+
+found:
+ out->ptr = buf.ptr;
+ out->asize = buf.asize;
+ out->size = buf.size;
+
+cleanup:
+ {
+ size_t i;
+ struct possible_tag *match;
+ git_vector_foreach(&all_matches, i, match) {
+ git__free(match);
+ }
+ }
+ git_vector_free(&all_matches);
+ git_pqueue_free(&list);
+ git_revwalk_free(walk);
+ return error;
+}
+
+static int normalize_options(
+ git_describe_opts *dst,
+ const git_describe_opts *src)
+{
+ git_describe_opts default_options = GIT_DESCRIBE_OPTIONS_INIT;
+ if (!src) src = &default_options;
+
+ *dst = *src;
+
+ if (dst->max_candidates_tags > GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS)
+ dst->max_candidates_tags = GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS;
+
+ if (dst->always_use_long_format && dst->abbreviated_size == 0) {
+ giterr_set(GITERR_DESCRIBE, "Cannot describe - "
+ "'always_use_long_format' is incompatible with a zero"
+ "'abbreviated_size'");
+ return -1;
+ }
+
+ return 0;
+}
+
+/** TODO: Add git_object_describe_workdir(git_buf *, const char *dirty_suffix, git_describe_opts *); */
+
+int git_describe_object(
+ git_buf *out,
+ git_object *committish,
+ git_describe_opts *opts)
+{
+ struct get_name_data data;
+ struct commit_name *name;
+ git_commit *commit;
+ int error = -1;
+ const char *dirty_suffix = NULL;
+ git_describe_opts normOptions;
+
+ assert(out && committish);
+
+ data.opts = opts;
+ data.repo = git_object_owner(committish);
+
+ if ((error = normalize_options(&normOptions, opts)) < 0)
+ return error;
+
+ GITERR_CHECK_VERSION(
+ &normOptions,
+ GIT_DESCRIBE_OPTIONS_VERSION,
+ "git_describe_opts");
+
+ data.names = git_oidmap_alloc();
+ GITERR_CHECK_ALLOC(data.names);
+
+ /** TODO: contains to be implemented */
+
+ /** TODO: deal with max_abbrev_size (either document or fix) */
+
+ if ((error = git_object_peel((git_object **)(&commit), committish, GIT_OBJ_COMMIT)) < 0)
+ goto cleanup;
+
+ if (git_reference_foreach_name(
+ git_object_owner(committish),
+ get_name, &data) < 0)
+ goto cleanup;
+
+ if (git_oidmap_size(data.names) == 0) {
+ giterr_set(GITERR_DESCRIBE, "Cannot describe - "
+ "No reference found, cannot describe anything.");
+ error = -1;
+ goto cleanup;
+ }
+
+ if ((error = describe(out, &data, commit, dirty_suffix)) < 0)
+ goto cleanup;
+
+cleanup:
+ git_oidmap_foreach_value(data.names, name, {
+ git_tag_free(name->tag);
+ git__free(name->path);
+ git__free(name);
+ });
+
+ git_oidmap_free(data.names);
+ git_commit_free(commit);
+
+ return error;
+}
diff --git a/src/oidmap.h b/src/oidmap.h
index 50da54b1c..b871a7926 100644
--- a/src/oidmap.h
+++ b/src/oidmap.h
@@ -45,4 +45,7 @@ GIT_INLINE(khint_t) git_oidmap_hash(const git_oid *oid)
} } while (0)
#define git_oidmap_foreach_value kh_foreach_value
+
+#define git_oidmap_size(h) kh_size(h)
+
#endif
diff --git a/tests/describe/describe.c b/tests/describe/describe.c
new file mode 100644
index 000000000..fcd4486bc
--- /dev/null
+++ b/tests/describe/describe.c
@@ -0,0 +1,47 @@
+#include "clar_libgit2.h"
+#include "describe_helpers.h"
+
+void test_describe_describe__can_describe_against_a_bare_repo(void)
+{
+ git_repository *repo;
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+
+ cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git")));
+
+ assert_describe("hard_tag", "HEAD", repo, &opts, false);
+
+ opts.show_commit_oid_as_fallback = 1;
+
+ assert_describe("be3563a", "HEAD^", repo, &opts, true);
+
+ git_repository_free(repo);
+}
+
+static int delete_cb(git_reference *ref, void *payload)
+{
+ GIT_UNUSED(payload);
+
+ cl_git_pass(git_reference_delete(ref));
+ git_reference_free(ref);
+
+ return 0;
+}
+
+void test_describe_describe__cannot_describe_against_a_repo_with_no_ref(void)
+{
+ git_repository *repo;
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ git_buf buf = GIT_BUF_INIT;
+ git_object *object;
+
+ repo = cl_git_sandbox_init("testrepo.git");
+ cl_git_pass(git_revparse_single(&object, repo, "HEAD"));
+
+ cl_git_pass(git_reference_foreach(repo, delete_cb, NULL));
+
+ cl_git_fail(git_describe_object(&buf, object, &opts));
+
+ git_object_free(object);
+ git_buf_free(&buf);
+ cl_git_sandbox_cleanup();
+}
diff --git a/tests/describe/describe_helpers.c b/tests/describe/describe_helpers.c
new file mode 100644
index 000000000..aa2a54f9e
--- /dev/null
+++ b/tests/describe/describe_helpers.c
@@ -0,0 +1,24 @@
+#include "describe_helpers.h"
+
+void assert_describe(
+ const char *expected_output,
+ const char *revparse_spec,
+ git_repository *repo,
+ git_describe_opts *opts,
+ bool is_prefix_match)
+{
+ git_object *object;
+ git_buf label;
+
+ cl_git_pass(git_revparse_single(&object, repo, revparse_spec));
+
+ cl_git_pass(git_describe_object(&label, object, opts));
+
+ if (is_prefix_match)
+ cl_assert_equal_i(0, git__prefixcmp(git_buf_cstr(&label), expected_output));
+ else
+ cl_assert_equal_s(expected_output, label);
+
+ git_object_free(object);
+ git_buf_free(&label);
+}
diff --git a/tests/describe/describe_helpers.h b/tests/describe/describe_helpers.h
new file mode 100644
index 000000000..0f107f5a7
--- /dev/null
+++ b/tests/describe/describe_helpers.h
@@ -0,0 +1,9 @@
+#include "clar_libgit2.h"
+#include "buffer.h"
+
+extern void assert_describe(
+ const char *expected_output,
+ const char *revparse_spec,
+ git_repository *repo,
+ git_describe_opts *opts,
+ bool is_prefix_match);
diff --git a/tests/describe/t6120.c b/tests/describe/t6120.c
new file mode 100644
index 000000000..d589e82a6
--- /dev/null
+++ b/tests/describe/t6120.c
@@ -0,0 +1,135 @@
+#include "clar_libgit2.h"
+#include "describe_helpers.h"
+#include "repository.h"
+
+// Ported from https://github.com/git/git/blob/adfc1857bdb090786fd9d22c1acec39371c76048/t/t6120-describe.sh
+
+static git_repository *repo;
+
+void test_describe_t6120__initialize(void)
+{
+ repo = cl_git_sandbox_init("describe");
+}
+
+void test_describe_t6120__cleanup(void)
+{
+ cl_git_sandbox_cleanup();
+}
+
+void test_describe_t6120__default(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+
+ assert_describe("A-", "HEAD", repo, &opts, true);
+ assert_describe("A-", "HEAD^", repo, &opts, true);
+ assert_describe("R-", "HEAD^^", repo, &opts, true);
+ assert_describe("A-", "HEAD^^2", repo, &opts, true);
+ assert_describe("B", "HEAD^^2^", repo, &opts, false);
+ assert_describe("R-", "HEAD^^^", repo, &opts, true);
+}
+
+void test_describe_t6120__tags(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ opts.describe_strategy = GIT_DESCRIBE_TAGS;
+
+ assert_describe("c-", "HEAD", repo, &opts, true);
+ assert_describe("c-", "HEAD^", repo, &opts, true);
+ assert_describe("e-", "HEAD^^", repo, &opts, true);
+ assert_describe("c-", "HEAD^^2", repo, &opts, true);
+ assert_describe("B", "HEAD^^2^", repo, &opts, false);
+ assert_describe("e", "HEAD^^^", repo, &opts, false);
+}
+
+void test_describe_t6120__all(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ opts.describe_strategy = GIT_DESCRIBE_ALL;
+
+ assert_describe("heads/master", "HEAD", repo, &opts, false);
+ assert_describe("tags/c-", "HEAD^", repo, &opts, true);
+ assert_describe("tags/e", "HEAD^^^", repo, &opts, false);
+}
+
+void test_describe_t6120__longformat(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ opts.always_use_long_format = 1;
+
+ assert_describe("B-0-", "HEAD^^2^", repo, &opts, true);
+ assert_describe("A-3-", "HEAD^^2", repo, &opts, true);
+}
+
+void test_describe_t6120__firstparent(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ opts.describe_strategy = GIT_DESCRIBE_TAGS;
+
+ assert_describe("c-7-", "HEAD", repo, &opts, true);
+
+ opts.only_follow_first_parent = 1;
+ assert_describe("e-3-", "HEAD", repo, &opts, true);
+}
+
+static void commit_and_tag(
+ git_time_t *time,
+ const char *commit_msg,
+ const char *tag_name)
+{
+ git_index *index;
+ git_oid commit_id;
+ git_reference *ref;
+
+ cl_git_pass(git_repository_index__weakptr(&index, repo));
+
+ cl_git_append2file("describe/file", "\n");
+
+ git_index_add_bypath(index, "describe/file");
+ git_index_write(index);
+
+ *time += 10;
+ cl_repo_commit_from_index(&commit_id, repo, NULL, *time, commit_msg);
+
+ if (tag_name == NULL)
+ return;
+
+ cl_git_pass(git_reference_create(&ref, repo, tag_name, &commit_id, 0, NULL, NULL));
+ git_reference_free(ref);
+}
+
+void test_describe_t6120__pattern(void)
+{
+ git_describe_opts opts = GIT_DESCRIBE_OPTIONS_INIT;
+ git_oid tag_id;
+ git_object *head;
+ git_signature *tagger;
+ git_time_t time;
+
+ /* set-up matching pattern tests */
+ cl_git_pass(git_revparse_single(&head, repo, "HEAD"));
+
+ time = 1380553019;
+ cl_git_pass(git_signature_new(&tagger, "tagger", "tagger@libgit2.org", time, 0));
+ cl_git_pass(git_tag_create(&tag_id, repo, "test-annotated", head, tagger, "test-annotated", 0));
+ git_signature_free(tagger);
+ git_object_free(head);
+
+ commit_and_tag(&time, "one more", "refs/tags/test1-lightweight");
+ commit_and_tag(&time, "yet another", "refs/tags/test2-lightweight");
+ commit_and_tag(&time, "even more", NULL);
+
+
+ /* Exercize */
+ opts.pattern = "test-*";
+ assert_describe("test-annotated-", "HEAD", repo, &opts, true);
+
+ opts.describe_strategy = GIT_DESCRIBE_TAGS;
+ opts.pattern = "test1-*";
+ assert_describe("test1-lightweight-", "HEAD", repo, &opts, true);
+
+ opts.pattern = "test2-*";
+ assert_describe("test2-lightweight-", "HEAD", repo, &opts, true);
+
+ opts.always_use_long_format = 1;
+ assert_describe("test2-lightweight-", "HEAD^", repo, &opts, true);
+}