blob: 409cdbd3c227ac8499d5d455a086c169f9522373 [file] [log] [blame]
/*
* 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));
GIT_ERROR_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));
GIT_ERROR_CHECK_ALLOC(entry);
assert(mm && replace_email && *replace_email);
if (real_name_size > 0) {
entry->real_name = git__substrdup(real_name, real_name_size);
GIT_ERROR_CHECK_ALLOC(entry->real_name);
}
if (real_email_size > 0) {
entry->real_email = git__substrdup(real_email, real_email_size);
GIT_ERROR_CHECK_ALLOC(entry->real_email);
}
if (replace_name_size > 0) {
entry->replace_name = git__substrdup(replace_name, replace_name_size);
GIT_ERROR_CHECK_ALLOC(entry->replace_name);
}
entry->replace_email = git__substrdup(replace_email, replace_email_size);
GIT_ERROR_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 = 0;
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_OBJECT_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;
}