diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/blame.c | 15 | ||||
| -rw-r--r-- | src/blame.h | 1 | ||||
| -rw-r--r-- | src/commit.c | 13 | ||||
| -rw-r--r-- | src/mailmap.c | 485 | ||||
| -rw-r--r-- | src/mailmap.h | 35 |
5 files changed, 545 insertions, 4 deletions
diff --git a/src/blame.c b/src/blame.c index a923bf003..fc87bd1c1 100644 --- a/src/blame.c +++ b/src/blame.c @@ -14,6 +14,7 @@ #include "git2/diff.h" #include "git2/blob.h" #include "git2/signature.h" +#include "git2/mailmap.h" #include "util.h" #include "repository.h" #include "blame_git.h" @@ -132,6 +133,9 @@ git_blame* git_blame__alloc( return NULL; } + if (opts.flags & GIT_BLAME_USE_MAILMAP) + git_mailmap_from_repository(&gbr->mailmap, repo); + return gbr; } @@ -150,6 +154,8 @@ void git_blame_free(git_blame *blame) git_array_clear(blame->line_index); + git_mailmap_free(blame->mailmap); + git__free(blame->path); git_blob_free(blame->final_blob); git__free(blame); @@ -279,7 +285,7 @@ static int index_blob_lines(git_blame *blame) return blame->num_lines; } -static git_blame_hunk* hunk_from_entry(git_blame__entry *e) +static git_blame_hunk* hunk_from_entry(git_blame__entry *e, git_blame *blame) { git_blame_hunk *h = new_hunk( e->lno+1, e->num_lines, e->s_lno+1, e->suspect->path); @@ -289,8 +295,9 @@ static git_blame_hunk* hunk_from_entry(git_blame__entry *e) git_oid_cpy(&h->final_commit_id, git_commit_id(e->suspect->commit)); git_oid_cpy(&h->orig_commit_id, git_commit_id(e->suspect->commit)); - git_signature_dup(&h->final_signature, git_commit_author(e->suspect->commit)); - git_signature_dup(&h->orig_signature, git_commit_author(e->suspect->commit)); + git_commit_author_with_mailmap( + &h->final_signature, e->suspect->commit, blame->mailmap); + git_signature_dup(&h->orig_signature, h->final_signature); h->boundary = e->is_boundary ? 1 : 0; return h; } @@ -341,7 +348,7 @@ static int blame_internal(git_blame *blame) cleanup: for (ent = blame->ent; ent; ) { git_blame__entry *e = ent->next; - git_blame_hunk *h = hunk_from_entry(ent); + git_blame_hunk *h = hunk_from_entry(ent, blame); git_vector_insert(&blame->hunks, h); diff --git a/src/blame.h b/src/blame.h index 8fd3ee5b1..b31d2dc20 100644 --- a/src/blame.h +++ b/src/blame.h @@ -67,6 +67,7 @@ typedef struct git_blame__entry { struct git_blame { char *path; git_repository *repository; + git_mailmap *mailmap; git_blame_options options; git_vector hunks; diff --git a/src/commit.c b/src/commit.c index e0c090aa8..e0ba51d47 100644 --- a/src/commit.c +++ b/src/commit.c @@ -11,6 +11,7 @@ #include "git2/object.h" #include "git2/repository.h" #include "git2/signature.h" +#include "git2/mailmap.h" #include "git2/sys/commit.h" #include "odb.h" @@ -889,3 +890,15 @@ cleanup: git_buf_dispose(&commit); return error; } + +int git_commit_committer_with_mailmap( + git_signature **out, const git_commit *commit, const git_mailmap *mailmap) +{ + return git_mailmap_resolve_signature(out, mailmap, commit->committer); +} + +int git_commit_author_with_mailmap( + git_signature **out, const git_commit *commit, const git_mailmap *mailmap) +{ + return git_mailmap_resolve_signature(out, mailmap, commit->author); +} diff --git a/src/mailmap.c b/src/mailmap.c new file mode 100644 index 000000000..fe4d452d5 --- /dev/null +++ b/src/mailmap.c @@ -0,0 +1,485 @@ +/* + * 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 "mailmap.h" + +#include "common.h" +#include "path.h" +#include "repository.h" +#include "signature.h" +#include "git2/config.h" +#include "git2/revparse.h" +#include "blob.h" +#include "parse.h" + +#define MM_FILE ".mailmap" +#define MM_FILE_CONFIG "mailmap.file" +#define MM_BLOB_CONFIG "mailmap.blob" +#define MM_BLOB_DEFAULT "HEAD:" MM_FILE + +static void mailmap_entry_free(git_mailmap_entry *entry) +{ + if (!entry) + return; + + git__free(entry->real_name); + git__free(entry->real_email); + git__free(entry->replace_name); + git__free(entry->replace_email); + git__free(entry); +} + +/* + * First we sort by replace_email, then replace_name (if present). + * Entries with names are greater than entries without. + */ +static int mailmap_entry_cmp(const void *a_raw, const void *b_raw) +{ + const git_mailmap_entry *a = (const git_mailmap_entry *)a_raw; + const git_mailmap_entry *b = (const git_mailmap_entry *)b_raw; + int cmp; + + assert(a && b && a->replace_email && b->replace_email); + + cmp = git__strcmp(a->replace_email, b->replace_email); + if (cmp) + return cmp; + + /* NULL replace_names are less than not-NULL ones */ + if (a->replace_name == NULL || b->replace_name == NULL) + return (int)(a->replace_name != NULL) - (int)(b->replace_name != NULL); + + return git__strcmp(a->replace_name, b->replace_name); +} + +/* Replace the old entry with the new on duplicate. */ +static int mailmap_entry_replace(void **old_raw, void *new_raw) +{ + mailmap_entry_free((git_mailmap_entry *)*old_raw); + *old_raw = new_raw; + return GIT_EEXISTS; +} + +/* Check if we're at the end of line, w/ comments */ +static bool is_eol(git_parse_ctx *ctx) +{ + char c; + return git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE) < 0 || c == '#'; +} + +static int advance_until( + const char **start, size_t *len, git_parse_ctx *ctx, char needle) +{ + *start = ctx->line; + while (ctx->line_len > 0 && *ctx->line != '#' && *ctx->line != needle) + git_parse_advance_chars(ctx, 1); + + if (ctx->line_len == 0 || *ctx->line == '#') + return -1; /* end of line */ + + *len = ctx->line - *start; + git_parse_advance_chars(ctx, 1); /* advance past needle */ + return 0; +} + +/* + * Parse a single entry from a mailmap file. + * + * The output git_bufs will be non-owning, and should be copied before being + * persisted. + */ +static int parse_mailmap_entry( + git_buf *real_name, git_buf *real_email, + git_buf *replace_name, git_buf *replace_email, + git_parse_ctx *ctx) +{ + const char *start; + size_t len; + + git_buf_clear(real_name); + git_buf_clear(real_email); + git_buf_clear(replace_name); + git_buf_clear(replace_email); + + git_parse_advance_ws(ctx); + if (is_eol(ctx)) + return -1; /* blank line */ + + /* Parse the real name */ + if (advance_until(&start, &len, ctx, '<') < 0) + return -1; + + git_buf_attach_notowned(real_name, start, len); + git_buf_rtrim(real_name); + + /* + * If this is the last email in the line, this is the email to replace, + * otherwise, it's the real email. + */ + if (advance_until(&start, &len, ctx, '>') < 0) + return -1; + + /* If we aren't at the end of the line, parse a second name and email */ + if (!is_eol(ctx)) { + git_buf_attach_notowned(real_email, start, len); + + git_parse_advance_ws(ctx); + if (advance_until(&start, &len, ctx, '<') < 0) + return -1; + git_buf_attach_notowned(replace_name, start, len); + git_buf_rtrim(replace_name); + + if (advance_until(&start, &len, ctx, '>') < 0) + return -1; + } + + git_buf_attach_notowned(replace_email, start, len); + + if (!is_eol(ctx)) + return -1; + + return 0; +} + +int git_mailmap_new(git_mailmap **out) +{ + int error; + git_mailmap *mm = git__calloc(1, sizeof(git_mailmap)); + GITERR_CHECK_ALLOC(mm); + + error = git_vector_init(&mm->entries, 0, mailmap_entry_cmp); + if (error < 0) { + git__free(mm); + return error; + } + *out = mm; + return 0; +} + +void git_mailmap_free(git_mailmap *mm) +{ + size_t idx; + git_mailmap_entry *entry; + if (!mm) + return; + + git_vector_foreach(&mm->entries, idx, entry) + mailmap_entry_free(entry); + + git_vector_free(&mm->entries); + git__free(mm); +} + +static int mailmap_add_entry_unterminated( + git_mailmap *mm, + const char *real_name, size_t real_name_size, + const char *real_email, size_t real_email_size, + const char *replace_name, size_t replace_name_size, + const char *replace_email, size_t replace_email_size) +{ + int error; + git_mailmap_entry *entry = git__calloc(1, sizeof(git_mailmap_entry)); + GITERR_CHECK_ALLOC(entry); + + assert(mm && replace_email && *replace_email); + + if (real_name_size > 0) { + entry->real_name = git__substrdup(real_name, real_name_size); + GITERR_CHECK_ALLOC(entry->real_name); + } + if (real_email_size > 0) { + entry->real_email = git__substrdup(real_email, real_email_size); + GITERR_CHECK_ALLOC(entry->real_email); + } + if (replace_name_size > 0) { + entry->replace_name = git__substrdup(replace_name, replace_name_size); + GITERR_CHECK_ALLOC(entry->replace_name); + } + entry->replace_email = git__substrdup(replace_email, replace_email_size); + GITERR_CHECK_ALLOC(entry->replace_email); + + error = git_vector_insert_sorted(&mm->entries, entry, mailmap_entry_replace); + if (error == GIT_EEXISTS) + error = GIT_OK; + else if (error < 0) + mailmap_entry_free(entry); + + return error; +} + +int git_mailmap_add_entry( + git_mailmap *mm, const char *real_name, const char *real_email, + const char *replace_name, const char *replace_email) +{ + return mailmap_add_entry_unterminated( + mm, + real_name, real_name ? strlen(real_name) : 0, + real_email, real_email ? strlen(real_email) : 0, + replace_name, replace_name ? strlen(replace_name) : 0, + replace_email, strlen(replace_email)); +} + +static int mailmap_add_buffer(git_mailmap *mm, const char *buf, size_t len) +{ + int error; + git_parse_ctx ctx; + + /* Scratch buffers containing the real parsed names & emails */ + git_buf real_name = GIT_BUF_INIT; + git_buf real_email = GIT_BUF_INIT; + git_buf replace_name = GIT_BUF_INIT; + git_buf replace_email = GIT_BUF_INIT; + + /* Buffers may not contain '\0's. */ + if (memchr(buf, '\0', len) != NULL) + return -1; + + git_parse_ctx_init(&ctx, buf, len); + + /* Run the parser */ + while (ctx.remain_len > 0) { + error = parse_mailmap_entry( + &real_name, &real_email, &replace_name, &replace_email, &ctx); + if (error < 0) { + error = 0; /* Skip lines which don't contain a valid entry */ + git_parse_advance_line(&ctx); + continue; /* TODO: warn */ + } + + /* NOTE: Can't use add_entry(...) as our buffers aren't terminated */ + error = mailmap_add_entry_unterminated( + mm, real_name.ptr, real_name.size, real_email.ptr, real_email.size, + replace_name.ptr, replace_name.size, replace_email.ptr, replace_email.size); + if (error < 0) + goto cleanup; + + error = 0; + } + +cleanup: + git_buf_dispose(&real_name); + git_buf_dispose(&real_email); + git_buf_dispose(&replace_name); + git_buf_dispose(&replace_email); + return error; +} + +int git_mailmap_from_buffer(git_mailmap **out, const char *data, size_t len) +{ + int error = git_mailmap_new(out); + if (error < 0) + return error; + + error = mailmap_add_buffer(*out, data, len); + if (error < 0) { + git_mailmap_free(*out); + *out = NULL; + } + return error; +} + +static int mailmap_add_blob( + git_mailmap *mm, git_repository *repo, const char *rev) +{ + git_object *object = NULL; + git_blob *blob = NULL; + git_buf content = GIT_BUF_INIT; + int error; + + assert(mm && repo); + + error = git_revparse_single(&object, repo, rev); + if (error < 0) + goto cleanup; + + error = git_object_peel((git_object **)&blob, object, GIT_OBJ_BLOB); + if (error < 0) + goto cleanup; + + error = git_blob__getbuf(&content, blob); + if (error < 0) + goto cleanup; + + error = mailmap_add_buffer(mm, content.ptr, content.size); + if (error < 0) + goto cleanup; + +cleanup: + git_buf_dispose(&content); + git_blob_free(blob); + git_object_free(object); + return error; +} + +static int mailmap_add_file_ondisk( + git_mailmap *mm, const char *path, git_repository *repo) +{ + const char *base = repo ? git_repository_workdir(repo) : NULL; + git_buf fullpath = GIT_BUF_INIT; + git_buf content = GIT_BUF_INIT; + int error; + + error = git_path_join_unrooted(&fullpath, path, base, NULL); + if (error < 0) + goto cleanup; + + error = git_futils_readbuffer(&content, fullpath.ptr); + if (error < 0) + goto cleanup; + + error = mailmap_add_buffer(mm, content.ptr, content.size); + if (error < 0) + goto cleanup; + +cleanup: + git_buf_dispose(&fullpath); + git_buf_dispose(&content); + return error; +} + +/* NOTE: Only expose with an error return, currently never errors */ +static void mailmap_add_from_repository(git_mailmap *mm, git_repository *repo) +{ + git_config *config = NULL; + git_buf rev_buf = GIT_BUF_INIT; + git_buf path_buf = GIT_BUF_INIT; + const char *rev = NULL; + const char *path = NULL; + + assert(mm && repo); + + /* If we're in a bare repo, default blob to 'HEAD:.mailmap' */ + if (repo->is_bare) + rev = MM_BLOB_DEFAULT; + + /* Try to load 'mailmap.file' and 'mailmap.blob' cfgs from the repo */ + if (git_repository_config(&config, repo) == 0) { + if (git_config_get_string_buf(&rev_buf, config, MM_BLOB_CONFIG) == 0) + rev = rev_buf.ptr; + if (git_config_get_path(&path_buf, config, MM_FILE_CONFIG) == 0) + path = path_buf.ptr; + } + + /* + * Load mailmap files in order, overriding previous entries with new ones. + * 1. The '.mailmap' file in the repository's workdir root, + * 2. The blob described by the 'mailmap.blob' config (default HEAD:.mailmap), + * 3. The file described by the 'mailmap.file' config. + * + * We ignore errors from these loads, as these files may not exist, or may + * contain invalid information, and we don't want to report that error. + * + * XXX: Warn? + */ + if (!repo->is_bare) + mailmap_add_file_ondisk(mm, MM_FILE, repo); + if (rev != NULL) + mailmap_add_blob(mm, repo, rev); + if (path != NULL) + mailmap_add_file_ondisk(mm, path, repo); + + git_buf_dispose(&rev_buf); + git_buf_dispose(&path_buf); + git_config_free(config); +} + +int git_mailmap_from_repository(git_mailmap **out, git_repository *repo) +{ + int error = git_mailmap_new(out); + if (error < 0) + return error; + mailmap_add_from_repository(*out, repo); + return 0; +} + +const git_mailmap_entry *git_mailmap_entry_lookup( + const git_mailmap *mm, const char *name, const char *email) +{ + int error; + ssize_t fallback = -1; + size_t idx; + git_mailmap_entry *entry; + + /* The lookup needle we want to use only sets the replace_email. */ + git_mailmap_entry needle = { NULL }; + needle.replace_email = (char *)email; + + assert(email); + + if (!mm) + return NULL; + + /* + * We want to find the place to start looking. so we do a binary search for + * the "fallback" nameless entry. If we find it, we advance past it and record + * the index. + */ + error = git_vector_bsearch(&idx, (git_vector *)&mm->entries, &needle); + if (error >= 0) + fallback = idx++; + else if (error != GIT_ENOTFOUND) + return NULL; + + /* do a linear search for an exact match */ + for (; idx < git_vector_length(&mm->entries); ++idx) { + entry = git_vector_get(&mm->entries, idx); + + if (git__strcmp(entry->replace_email, email)) + break; /* it's a different email, so we're done looking */ + + assert(entry->replace_name); /* should be specific */ + if (!name || !git__strcmp(entry->replace_name, name)) + return entry; + } + + if (fallback < 0) + return NULL; /* no fallback */ + return git_vector_get(&mm->entries, fallback); +} + +int git_mailmap_resolve( + const char **real_name, const char **real_email, + const git_mailmap *mailmap, + const char *name, const char *email) +{ + const git_mailmap_entry *entry = NULL; + assert(name && email); + + *real_name = name; + *real_email = email; + + if ((entry = git_mailmap_entry_lookup(mailmap, name, email))) { + if (entry->real_name) + *real_name = entry->real_name; + if (entry->real_email) + *real_email = entry->real_email; + } + return 0; +} + +int git_mailmap_resolve_signature( + git_signature **out, const git_mailmap *mailmap, const git_signature *sig) +{ + const char *name = NULL; + const char *email = NULL; + int error; + + if (!sig) + return 0; + + error = git_mailmap_resolve(&name, &email, mailmap, sig->name, sig->email); + if (error < 0) + return error; + + error = git_signature_new(out, name, email, sig->when.time, sig->when.offset); + if (error < 0) + return error; + + /* Copy over the sign, as git_signature_new doesn't let you pass it. */ + (*out)->when.sign = sig->when.sign; + return 0; +} diff --git a/src/mailmap.h b/src/mailmap.h new file mode 100644 index 000000000..2c9736a4a --- /dev/null +++ b/src/mailmap.h @@ -0,0 +1,35 @@ +/* + * 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_mailmap_h__ +#define INCLUDE_mailmap_h__ + +#include "git2/mailmap.h" +#include "vector.h" + +/* + * A mailmap is stored as a sorted vector of 'git_mailmap_entry's. These entries + * are sorted first by 'replace_email', and then by 'replace_name'. NULL + * replace_names are ordered first. + * + * Looking up a name and email in the mailmap is done with a binary search. + */ +struct git_mailmap { + git_vector entries; +}; + +/* Single entry parsed from a mailmap */ +typedef struct git_mailmap_entry { + char *real_name; /**< the real name (may be NULL) */ + char *real_email; /**< the real email (may be NULL) */ + char *replace_name; /**< the name to replace (may be NULL) */ + char *replace_email; /**< the email to replace */ +} git_mailmap_entry; + +const git_mailmap_entry *git_mailmap_entry_lookup( + const git_mailmap *mm, const char *name, const char *email); + +#endif |
