Commit 46a2b8e8 by Carlos Martín Nieto

Merge pull request #2592 from libgit2/cmn/describe

Implement git-describe
parents 324154a4 a3b9270d
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
#include "git2/commit.h" #include "git2/commit.h"
#include "git2/common.h" #include "git2/common.h"
#include "git2/config.h" #include "git2/config.h"
#include "git2/describe.h"
#include "git2/diff.h" #include "git2/diff.h"
#include "git2/errors.h" #include "git2/errors.h"
#include "git2/filter.h" #include "git2/filter.h"
......
...@@ -83,6 +83,8 @@ GIT_BEGIN_DECL ...@@ -83,6 +83,8 @@ GIT_BEGIN_DECL
*/ */
#define GIT_OID_HEX_ZERO "0000000000000000000000000000000000000000" #define GIT_OID_HEX_ZERO "0000000000000000000000000000000000000000"
#define FLAG_BITS 27
/** /**
* Return the version of the libgit2 library * Return the version of the libgit2 library
* being currently used. * being currently used.
......
/*
* 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
/**
* Reference lookup strategy
*
* These behave like the --tags and --all optios to git-describe,
* namely they say to look for any reference in either refs/tags/ or
* refs/ respectively.
*/
typedef enum {
GIT_DESCRIBE_DEFAULT,
GIT_DESCRIBE_TAGS,
GIT_DESCRIBE_ALL,
} git_describe_strategy_t;
/**
* Describe options structure
*
* Initialize with `GIT_DESCRIBE_OPTIONS_INIT` macro to correctly set
* the `version` field. E.g.
*
* git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
*/
typedef struct git_describe_options {
unsigned int version;
unsigned int max_candidates_tags; /** default: 10 */
unsigned int describe_strategy; /** default: GIT_DESCRIBE_DEFAULT */
const char *pattern;
/**
* When calculating the distance from the matching tag or
* reference, only walk down the first-parent ancestry.
*/
int only_follow_first_parent;
/**
* If no matching tag or reference is found, the describe
* operation would normally fail. If this option is set, it
* will instead fall back to showing the full id of the
* commit.
*/
int show_commit_oid_as_fallback;
} git_describe_options;
#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_EXTERN(int) git_describe_init_options(git_describe_options *opts, unsigned int version);
/**
* Options for formatting the describe string
*/
typedef struct {
unsigned int version;
/**
* Size of the abbreviated commit id to use. This value is the
* lower bound for the length of the abbreviated string. The
* default is 7.
*/
unsigned int abbreviated_size;
/**
* Set to use the long format even when a shorter name could be used.
*/
int always_use_long_format;
/**
* If the workdir is dirty and this is set, this string will
* be appended to the description string.
*/
char *dirty_suffix;
} git_describe_format_options;
#define GIT_DESCRIBE_FORMAT_OPTIONS_VERSION 1
#define GIT_DESCRIBE_FORMAT_OPTIONS_INIT { \
GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, \
GIT_DESCRIBE_DEFAULT_ABBREVIATED_SIZE, \
}
GIT_EXTERN(int) git_describe_init_format_options(git_describe_format_options *opts, unsigned int version);
typedef struct git_describe_result git_describe_result;
/**
* Describe a commit
*
* Perform the describe operation on the given committish object.
*
* @param result pointer to store the result. You must free this once
* you're done with it.
* @param committish a committish to describe
* @param opts the lookup options
*/
GIT_EXTERN(int) git_describe_commit(
git_describe_result **result,
git_object *committish,
git_describe_options *opts);
/**
* Describe a commit
*
* Perform the describe operation on the current commit and the
* worktree. After peforming describe on HEAD, a status is run and the
* description is considered to be dirty if there are.
*
* @param result pointer to store the result. You must free this once
* you're done with it.
* @param repo the repository in which to perform the describe
* @param opts the lookup options
*/
GIT_EXTERN(int) git_describe_workdir(
git_describe_result **out,
git_repository *repo,
git_describe_options *opts);
/**
* Print the describe result to a buffer
*
* @param result the result from `git_describe_commit()` or
* `git_describe_workdir()`.
* @param opt the formatting options
*/
GIT_EXTERN(int) git_describe_format(
git_buf *out,
const git_describe_result *result,
const git_describe_format_options *opts);
/**
* Free the describe result.
*/
GIT_EXTERN(void) git_describe_result_free(git_describe_result *result);
/** @} */
GIT_END_DECL
#endif
...@@ -89,6 +89,7 @@ typedef enum { ...@@ -89,6 +89,7 @@ typedef enum {
GITERR_REVERT, GITERR_REVERT,
GITERR_CALLBACK, GITERR_CALLBACK,
GITERR_CHERRYPICK, GITERR_CHERRYPICK,
GITERR_DESCRIBE,
} git_error_t; } git_error_t;
/** /**
......
...@@ -25,7 +25,7 @@ typedef struct git_commit_list_node { ...@@ -25,7 +25,7 @@ typedef struct git_commit_list_node {
uninteresting:1, uninteresting:1,
topo_delay:1, topo_delay:1,
parsed:1, parsed:1,
flags : 4; flags : FLAG_BITS;
unsigned short in_degree; unsigned short in_degree;
unsigned short out_degree; unsigned short out_degree;
......
/*
* 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 "git2/strarray.h"
#include "git2/diff.h"
#include "git2/status.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"
#include "repository.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 git_describe_result {
int dirty;
int exact_match;
int fallback_to_id;
git_oid commit_id;
git_repository *repo;
struct commit_name *name;
struct possible_tag *tag;
};
struct get_name_data
{
git_describe_options *opts;
git_repository *repo;
git_oidmap *names;
git_describe_result *result;
};
static int commit_name_dup(struct commit_name **out, struct commit_name *in)
{
struct commit_name *name;
name = git__malloc(sizeof(struct commit_name));
GITERR_CHECK_ALLOC(name);
memcpy(name, in, sizeof(struct commit_name));
name->tag = NULL;
name->path = NULL;
if (in->tag && git_object_dup((git_object **) &name->tag, (git_object *) in->tag) < 0)
return -1;
name->path = git__strdup(in->path);
GITERR_CHECK_ALLOC(name->path);
*out = name;
return 0;
}
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 possible_tag_dup(struct possible_tag **out, struct possible_tag *in)
{
struct possible_tag *tag;
tag = git__malloc(sizeof(struct possible_tag));
GITERR_CHECK_ALLOC(tag);
memcpy(tag, in, sizeof(struct possible_tag));
tag->name = NULL;
if (commit_name_dup(&tag->name, in->name) < 0)
return -1;
*out = tag;
return 0;
}
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,
git_repository *repo,
const git_oid *oid_in,
int abbreviated_size)
{
size_t size = abbreviated_size;
git_odb *odb;
git_oid dummy;
int error;
if ((error = git_repository_odb__weakptr(&odb, repo)) < 0)
return error;
while (size < GIT_OID_HEXSZ) {
if ((error = git_odb_exists_prefix(&dummy, odb, oid_in, size)) == 0) {
*out = (int) size;
return 0;
}
/* If the error wasn't that it's not unique, then it's a proper error */
if (error != GIT_EAMBIGUOUS)
return error;
/* Try again with a larger size */
size++;
}
/* If we didn't find any shorter prefix, we have to do the whole thing */
*out = GIT_OID_HEXSZ;
return 0;
}
static int show_suffix(
git_buf *buf,
int depth,
git_repository *repo,
const git_oid* id,
size_t abbrev_size)
{
int error, size;
char hex_oid[GIT_OID_HEXSZ];
if ((error = find_unique_abbrev_size(&size, repo, 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);
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(
struct get_name_data *data,
git_commit *commit)
{
struct commit_name *n;
struct possible_tag *best;
bool all, tags;
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;
git_oid_cpy(&data->result->commit_id, git_commit_id(commit));
n = find_commit_name(data->names, git_commit_id(commit));
if (n && (tags || all || n->prio == 2)) {
/*
* Exact match to an existing ref.
*/
data->result->exact_match = 1;
if ((error = commit_name_dup(&data->result->name, n)) < 0)
goto cleanup;
goto cleanup;
}
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) {
data->result->fallback_to_id = 1;
git_oid_cpy(&data->result->commit_id, &cmit->oid);
goto cleanup;
}
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;
if ((error = possible_tag_dup(&data->result->tag, best)) < 0)
goto cleanup;
/*
{
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);
}
}
}
*/
git_oid_cpy(&data->result->commit_id, &cmit->oid);
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_options *dst,
const git_describe_options *src)
{
git_describe_options 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;
return 0;
}
int git_describe_commit(
git_describe_result **result,
git_object *committish,
git_describe_options *opts)
{
struct get_name_data data;
struct commit_name *name;
git_commit *commit;
int error = -1;
git_describe_options normalized;
assert(committish);
data.result = git__calloc(1, sizeof(git_describe_result));
GITERR_CHECK_ALLOC(data.result);
data.result->repo = git_object_owner(committish);
data.opts = opts;
data.repo = git_object_owner(committish);
if ((error = normalize_options(&normalized, opts)) < 0)
return error;
GITERR_CHECK_VERSION(
&normalized,
GIT_DESCRIBE_OPTIONS_VERSION,
"git_describe_options");
data.names = git_oidmap_alloc();
GITERR_CHECK_ALLOC(data.names);
/** TODO: contains to be implemented */
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(&data, commit)) < 0)
goto cleanup;
cleanup:
git_commit_free(commit);
git_oidmap_foreach_value(data.names, name, {
git_tag_free(name->tag);
git__free(name->path);
git__free(name);
});
git_oidmap_free(data.names);
if (error < 0)
git_describe_result_free(data.result);
else
*result = data.result;
return error;
}
int git_describe_workdir(
git_describe_result **out,
git_repository *repo,
git_describe_options *opts)
{
int error;
git_oid current_id;
git_status_list *status = NULL;
git_status_options status_opts = GIT_STATUS_OPTIONS_INIT;
git_describe_result *result;
git_object *commit;
if ((error = git_reference_name_to_id(&current_id, repo, GIT_HEAD_FILE)) < 0)
return error;
if ((error = git_object_lookup(&commit, repo, &current_id, GIT_OBJ_COMMIT)) < 0)
return error;
/* The first step is to perform a describe of HEAD, so we can leverage this */
if ((error = git_describe_commit(&result, commit, opts)) < 0)
goto out;
if ((error = git_status_list_new(&status, repo, &status_opts)) < 0)
goto out;
if (git_status_list_entrycount(status) > 0)
result->dirty = 1;
out:
git_object_free(commit);
git_status_list_free(status);
if (error < 0)
git_describe_result_free(result);
else
*out = result;
return error;
}
int git_describe_format(git_buf *out, const git_describe_result *result, const git_describe_format_options *opts)
{
int error;
git_repository *repo;
struct commit_name *name;
assert(out && result);
GITERR_CHECK_VERSION(opts, GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, "git_describe_format_options");
git_buf_sanitize(out);
if (opts->always_use_long_format && opts->abbreviated_size == 0) {
giterr_set(GITERR_DESCRIBE, "Cannot describe - "
"'always_use_long_format' is incompatible with a zero"
"'abbreviated_size'");
return -1;
}
repo = result->repo;
/* If we did find an exact match, then it's the easier method */
if (result->exact_match) {
name = result->name;
if ((error = display_name(out, repo, name)) < 0)
return error;
if (opts->always_use_long_format) {
const git_oid *id = name->tag ? git_tag_target_id(name->tag) : &result->commit_id;
if ((error = show_suffix(out, 0, repo, id, opts->abbreviated_size)) < 0)
return error;
}
if (result->dirty && opts->dirty_suffix)
git_buf_puts(out, opts->dirty_suffix);
return git_buf_oom(out) ? -1 : 0;
}
/* If we didn't find *any* tags, we fall back to the commit's id */
if (result->fallback_to_id) {
char hex_oid[GIT_OID_HEXSZ + 1] = {0};
int size;
if ((error = find_unique_abbrev_size(
&size, repo, &result->commit_id, opts->abbreviated_size)) < 0)
return -1;
git_oid_fmt(hex_oid, &result->commit_id);
git_buf_put(out, hex_oid, size);
if (result->dirty && opts->dirty_suffix)
git_buf_puts(out, opts->dirty_suffix);
return git_buf_oom(out) ? -1 : 0;
}
/* Lastly, if we found a matching tag, we show that */
name = result->tag->name;
if ((error = display_name(out, repo, name)) < 0)
return error;
if (opts->abbreviated_size) {
if ((error = show_suffix(out, result->tag->depth, repo,
&result->commit_id, opts->abbreviated_size)) < 0)
return error;
}
if (result->dirty && opts->dirty_suffix) {
git_buf_puts(out, opts->dirty_suffix);
}
return git_buf_oom(out) ? -1 : 0;
}
void git_describe_result_free(git_describe_result *result)
{
if (result == NULL)
return;
if (result->name) {
git_tag_free(result->name->tag);
git__free(result->name->path);
git__free(result->name);
}
if (result->tag) {
git_tag_free(result->tag->name->tag);
git__free(result->tag->name->path);
git__free(result->tag->name);
git__free(result->tag);
}
git__free(result);
}
int git_describe_init_options(git_describe_options *opts, unsigned int version)
{
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
opts, version, git_describe_options, GIT_DESCRIBE_OPTIONS_INIT);
return 0;
}
int git_describe_init_format_options(git_describe_format_options *opts, unsigned int version)
{
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
opts, version, git_describe_format_options, GIT_DESCRIBE_FORMAT_OPTIONS_INIT);
return 0;
}
...@@ -32,4 +32,20 @@ GIT_INLINE(khint_t) git_oidmap_hash(const git_oid *oid) ...@@ -32,4 +32,20 @@ GIT_INLINE(khint_t) git_oidmap_hash(const git_oid *oid)
#define git_oidmap_alloc() kh_init(oid) #define git_oidmap_alloc() kh_init(oid)
#define git_oidmap_free(h) kh_destroy(oid,h), h = NULL #define git_oidmap_free(h) kh_destroy(oid,h), h = NULL
#define git_oidmap_lookup_index(h, k) kh_get(oid, h, k)
#define git_oidmap_valid_index(h, idx) (idx != kh_end(h))
#define git_oidmap_value_at(h, idx) kh_val(h, idx)
#define git_oidmap_insert(h, key, val, rval) do { \
khiter_t __pos = kh_put(oid, h, key, &rval); \
if (rval >= 0) { \
if (rval == 0) kh_key(h, __pos) = key; \
kh_val(h, __pos) = val; \
} } while (0)
#define git_oidmap_foreach_value kh_foreach_value
#define git_oidmap_size(h) kh_size(h)
#endif #endif
#include "clar_libgit2.h"
#include "describe_helpers.h"
void test_describe_describe__can_describe_against_a_bare_repo(void)
{
git_repository *repo;
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git")));
assert_describe("hard_tag", "HEAD", repo, &opts, &fmt_opts);
opts.show_commit_oid_as_fallback = 1;
assert_describe("be3563a*", "HEAD^", repo, &opts, &fmt_opts);
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_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_buf buf = GIT_BUF_INIT;
git_object *object;
git_describe_result *result = NULL;
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_commit(&result, object, &opts));
git_describe_result_free(result);
git_object_free(object);
git_buf_free(&buf);
cl_git_sandbox_cleanup();
}
#include "describe_helpers.h"
void assert_describe(
const char *expected_output,
const char *revparse_spec,
git_repository *repo,
git_describe_options *opts,
git_describe_format_options *fmt_opts)
{
git_object *object;
git_buf label = GIT_BUF_INIT;
git_describe_result *result;
cl_git_pass(git_revparse_single(&object, repo, revparse_spec));
cl_git_pass(git_describe_commit(&result, object, opts));
cl_git_pass(git_describe_format(&label, result, fmt_opts));
cl_git_pass(p_fnmatch(expected_output, git_buf_cstr(&label), 0));
git_describe_result_free(result);
git_object_free(object);
git_buf_free(&label);
}
void assert_describe_workdir(
const char *expected_output,
git_repository *repo,
git_describe_options *opts,
git_describe_format_options *fmt_opts)
{
git_buf label = GIT_BUF_INIT;
git_describe_result *result;
cl_git_pass(git_describe_workdir(&result, repo, opts));
cl_git_pass(git_describe_format(&label, result, fmt_opts));
cl_git_pass(p_fnmatch(expected_output, git_buf_cstr(&label), 0));
git_describe_result_free(result);
git_buf_free(&label);
}
#include "clar_libgit2.h"
#include "buffer.h"
extern void assert_describe(
const char *expected_output,
const char *revparse_spec,
git_repository *repo,
git_describe_options *opts,
git_describe_format_options *fmt_opts);
extern void assert_describe_workdir(
const char *expected_output,
git_repository *repo,
git_describe_options *opts,
git_describe_format_options *fmt_opts);
#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_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
assert_describe("A-*", "HEAD", repo, &opts, &fmt_opts);
assert_describe("A-*", "HEAD^", repo, &opts, &fmt_opts);
assert_describe("R-*", "HEAD^^", repo, &opts, &fmt_opts);
assert_describe("A-*", "HEAD^^2", repo, &opts, &fmt_opts);
assert_describe("B", "HEAD^^2^", repo, &opts, &fmt_opts);
assert_describe("R-*", "HEAD^^^", repo, &opts, &fmt_opts);
}
void test_describe_t6120__tags(void)
{
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
opts.describe_strategy = GIT_DESCRIBE_TAGS;
assert_describe("c-*", "HEAD", repo, &opts, &fmt_opts);
assert_describe("c-*", "HEAD^", repo, &opts, &fmt_opts);
assert_describe("e-*", "HEAD^^", repo, &opts, &fmt_opts);
assert_describe("c-*", "HEAD^^2", repo, &opts, &fmt_opts);
assert_describe("B", "HEAD^^2^", repo, &opts, &fmt_opts);
assert_describe("e", "HEAD^^^", repo, &opts, &fmt_opts);
}
void test_describe_t6120__all(void)
{
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
opts.describe_strategy = GIT_DESCRIBE_ALL;
assert_describe("heads/master", "HEAD", repo, &opts, &fmt_opts);
assert_describe("tags/c-*", "HEAD^", repo, &opts, &fmt_opts);
assert_describe("tags/e", "HEAD^^^", repo, &opts, &fmt_opts);
}
void test_describe_t6120__longformat(void)
{
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
fmt_opts.always_use_long_format = 1;
assert_describe("B-0-*", "HEAD^^2^", repo, &opts, &fmt_opts);
assert_describe("A-3-*", "HEAD^^2", repo, &opts, &fmt_opts);
}
void test_describe_t6120__firstparent(void)
{
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
opts.describe_strategy = GIT_DESCRIBE_TAGS;
assert_describe("c-7-*", "HEAD", repo, &opts, &fmt_opts);
opts.only_follow_first_parent = 1;
assert_describe("e-3-*", "HEAD", repo, &opts, &fmt_opts);
}
void test_describe_t6120__workdir(void)
{
git_describe_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_OPTIONS_INIT;
assert_describe_workdir("A-*[0-9a-f]", repo, &opts, &fmt_opts);
cl_git_mkfile("describe/file", "something different");
fmt_opts.dirty_suffix = "-dirty";
assert_describe_workdir("A-*[0-9a-f]-dirty", repo, &opts, &fmt_opts);
fmt_opts.dirty_suffix = ".mod";
assert_describe_workdir("A-*[0-9a-f].mod", repo, &opts, &fmt_opts);
}
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_options opts = GIT_DESCRIBE_OPTIONS_INIT;
git_describe_format_options fmt_opts = GIT_DESCRIBE_FORMAT_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, &fmt_opts);
opts.describe_strategy = GIT_DESCRIBE_TAGS;
opts.pattern = "test1-*";
assert_describe("test1-lightweight-*", "HEAD", repo, &opts, &fmt_opts);
opts.pattern = "test2-*";
assert_describe("test2-lightweight-*", "HEAD", repo, &opts, &fmt_opts);
fmt_opts.always_use_long_format = 1;
assert_describe("test2-lightweight-*", "HEAD^", repo, &opts, &fmt_opts);
}
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
hideDotFiles = dotGitOnly
0000000000000000000000000000000000000000 108b485d8268ea595df8ffea74f0f4b186577d32 nulltoken <emeric.fermas@gmail.com> 1380209394 +0200 commit (initial): initial
108b485d8268ea595df8ffea74f0f4b186577d32 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209404 +0200 commit: second
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f b240c0fb88c5a629e00ebc1275fa1f33e364a705 nulltoken <emeric.fermas@gmail.com> 1380209414 +0200 commit: third
b240c0fb88c5a629e00ebc1275fa1f33e364a705 81f4b1aac643e6983fab370eae8aefccecbf3a4c nulltoken <emeric.fermas@gmail.com> 1380209425 +0200 commit: A
81f4b1aac643e6983fab370eae8aefccecbf3a4c 6126a5f9c57ebc81e64370ec3095184ad92dab1c nulltoken <emeric.fermas@gmail.com> 1380209445 +0200 commit: c
6126a5f9c57ebc81e64370ec3095184ad92dab1c 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209455 +0200 reset: moving to 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f 31fc9136820b507e938a9c6b88bf2c567a9f6f4b nulltoken <emeric.fermas@gmail.com> 1380209465 +0200 commit: B
31fc9136820b507e938a9c6b88bf2c567a9f6f4b ce1c4f8b6120122e23d4442925d98c56c41917d8 nulltoken <emeric.fermas@gmail.com> 1380209486 +0200 merge c: Merge made by the 'recursive' strategy.
ce1c4f8b6120122e23d4442925d98c56c41917d8 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209486 +0200 reset: moving to 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f 6a12b56088706aa6c39ccd23b7c7ce60f3a0b9a1 nulltoken <emeric.fermas@gmail.com> 1380209496 +0200 commit: D
6a12b56088706aa6c39ccd23b7c7ce60f3a0b9a1 1e016431ec7b22dd3e23f3e6f5f68f358f9227cf nulltoken <emeric.fermas@gmail.com> 1380209527 +0200 commit: another
1e016431ec7b22dd3e23f3e6f5f68f358f9227cf a9eb02af13df030159e39f70330d5c8a47655691 nulltoken <emeric.fermas@gmail.com> 1380209547 +0200 commit: yet another
a9eb02af13df030159e39f70330d5c8a47655691 949b98e208015bfc0e2f573debc34ae2f97a7f0e nulltoken <emeric.fermas@gmail.com> 1380209557 +0200 merge ce1c4f8b6120122e23d4442925d98c56c41917d8: Merge made by the 'recursive' strategy.
949b98e208015bfc0e2f573debc34ae2f97a7f0e a6095f816e81f64651595d488badc42399837d6a nulltoken <emeric.fermas@gmail.com> 1380209567 +0200 commit: x
0000000000000000000000000000000000000000 108b485d8268ea595df8ffea74f0f4b186577d32 nulltoken <emeric.fermas@gmail.com> 1380209394 +0200 commit (initial): initial
108b485d8268ea595df8ffea74f0f4b186577d32 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209404 +0200 commit: second
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f b240c0fb88c5a629e00ebc1275fa1f33e364a705 nulltoken <emeric.fermas@gmail.com> 1380209414 +0200 commit: third
b240c0fb88c5a629e00ebc1275fa1f33e364a705 81f4b1aac643e6983fab370eae8aefccecbf3a4c nulltoken <emeric.fermas@gmail.com> 1380209425 +0200 commit: A
81f4b1aac643e6983fab370eae8aefccecbf3a4c 6126a5f9c57ebc81e64370ec3095184ad92dab1c nulltoken <emeric.fermas@gmail.com> 1380209445 +0200 commit: c
6126a5f9c57ebc81e64370ec3095184ad92dab1c 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209455 +0200 reset: moving to 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f 31fc9136820b507e938a9c6b88bf2c567a9f6f4b nulltoken <emeric.fermas@gmail.com> 1380209465 +0200 commit: B
31fc9136820b507e938a9c6b88bf2c567a9f6f4b ce1c4f8b6120122e23d4442925d98c56c41917d8 nulltoken <emeric.fermas@gmail.com> 1380209486 +0200 merge c: Merge made by the 'recursive' strategy.
ce1c4f8b6120122e23d4442925d98c56c41917d8 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f nulltoken <emeric.fermas@gmail.com> 1380209486 +0200 reset: moving to 4d6558b8fa764baeb0f19c1e857df91e0eda5a0f
4d6558b8fa764baeb0f19c1e857df91e0eda5a0f 6a12b56088706aa6c39ccd23b7c7ce60f3a0b9a1 nulltoken <emeric.fermas@gmail.com> 1380209496 +0200 commit: D
6a12b56088706aa6c39ccd23b7c7ce60f3a0b9a1 1e016431ec7b22dd3e23f3e6f5f68f358f9227cf nulltoken <emeric.fermas@gmail.com> 1380209527 +0200 commit: another
1e016431ec7b22dd3e23f3e6f5f68f358f9227cf a9eb02af13df030159e39f70330d5c8a47655691 nulltoken <emeric.fermas@gmail.com> 1380209547 +0200 commit: yet another
a9eb02af13df030159e39f70330d5c8a47655691 949b98e208015bfc0e2f573debc34ae2f97a7f0e nulltoken <emeric.fermas@gmail.com> 1380209557 +0200 merge ce1c4f8b6120122e23d4442925d98c56c41917d8: Merge made by the 'recursive' strategy.
949b98e208015bfc0e2f573debc34ae2f97a7f0e a6095f816e81f64651595d488badc42399837d6a nulltoken <emeric.fermas@gmail.com> 1380209567 +0200 commit: x
a6095f816e81f64651595d488badc42399837d6a
aaddd4f14847e0e323924ec262c2343249a84f8b
52912fbab0715dec53d43053966e78ad213ba359
10bd08b099ecb79184c60183f5c94ca915f427ad
680166b6cd31f76354fee2572618e6b0142d05e6
6126a5f9c57ebc81e64370ec3095184ad92dab1c
1e016431ec7b22dd3e23f3e6f5f68f358f9227cf
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment