mailmap.c 12.1 KB
Newer Older
1 2 3 4 5 6 7
/*
 * 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.
 */

8
#include "mailmap.h"
9

10 11 12
#include "common.h"
#include "path.h"
#include "repository.h"
13
#include "signature.h"
14 15
#include "git2/config.h"
#include "git2/revparse.h"
16
#include "blob.h"
17
#include "parse.h"
18

19 20 21 22
#define MM_FILE ".mailmap"
#define MM_FILE_CONFIG "mailmap.file"
#define MM_BLOB_CONFIG "mailmap.blob"
#define MM_BLOB_DEFAULT "HEAD:" MM_FILE
23

24 25 26 27 28 29 30 31 32 33 34 35
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);
}

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
/*
 * 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;
}

67 68
/* Check if we're at the end of line, w/ comments */
static bool is_eol(git_parse_ctx *ctx)
69
{
70 71
	char c;
	return git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE) < 0 || c == '#';
72 73
}

74 75 76 77 78 79 80 81 82 83 84 85 86
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;
87 88
}

89 90
/*
 * Parse a single entry from a mailmap file.
91 92 93
 *
 * The output git_bufs will be non-owning, and should be copied before being
 * persisted.
94
 */
95 96 97 98
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)
99
{
100 101
	const char *start;
	size_t len;
102

103 104 105 106
	git_buf_clear(real_name);
	git_buf_clear(real_email);
	git_buf_clear(replace_name);
	git_buf_clear(replace_email);
107

108
	git_parse_advance_ws(ctx);
109 110 111 112
	if (is_eol(ctx))
		return -1; /* blank line */

	/* Parse the real name */
113 114
	if (advance_until(&start, &len, ctx, '<') < 0)
		return -1;
115

116 117
	git_buf_attach_notowned(real_name, start, len);
	git_buf_rtrim(real_name);
118

119 120 121 122
	/*
	 * If this is the last email in the line, this is the email to replace,
	 * otherwise, it's the real email.
	 */
123 124
	if (advance_until(&start, &len, ctx, '>') < 0)
		return -1;
125

126 127 128 129 130 131 132 133 134 135 136 137
	/* 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;
138
	}
139

140 141 142 143 144
	git_buf_attach_notowned(replace_email, start, len);

	if (!is_eol(ctx))
		return -1;

145 146
	return 0;
}
147

148 149 150 151
int git_mailmap_new(git_mailmap **out)
{
	int error;
	git_mailmap *mm = git__calloc(1, sizeof(git_mailmap));
152
	GIT_ERROR_CHECK_ALLOC(mm);
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

	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);
172 173

	git_vector_free(&mm->entries);
174 175 176
	git__free(mm);
}

177 178 179 180 181 182
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)
183 184 185
{
	int error;
	git_mailmap_entry *entry = git__calloc(1, sizeof(git_mailmap_entry));
186
	GIT_ERROR_CHECK_ALLOC(entry);
187 188 189

	assert(mm && replace_email && *replace_email);

190 191
	if (real_name_size > 0) {
		entry->real_name = git__substrdup(real_name, real_name_size);
192
		GIT_ERROR_CHECK_ALLOC(entry->real_name);
193
	}
194 195
	if (real_email_size > 0) {
		entry->real_email = git__substrdup(real_email, real_email_size);
196
		GIT_ERROR_CHECK_ALLOC(entry->real_email);
197
	}
198 199
	if (replace_name_size > 0) {
		entry->replace_name = git__substrdup(replace_name, replace_name_size);
200
		GIT_ERROR_CHECK_ALLOC(entry->replace_name);
201
	}
202
	entry->replace_email = git__substrdup(replace_email, replace_email_size);
203
	GIT_ERROR_CHECK_ALLOC(entry->replace_email);
204 205

	error = git_vector_insert_sorted(&mm->entries, entry, mailmap_entry_replace);
206 207 208
	if (error == GIT_EEXISTS)
		error = GIT_OK;
	else if (error < 0)
209 210 211 212 213
		mailmap_entry_free(entry);

	return error;
}

214 215 216 217 218 219 220 221 222 223 224 225
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));
}

226
static int mailmap_add_buffer(git_mailmap *mm, const char *buf, size_t len)
227
{
228
	int error = 0;
229 230 231 232 233 234 235 236
	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;

237 238
	/* Buffers may not contain '\0's. */
	if (memchr(buf, '\0', len) != NULL)
239
		return -1;
240

241
	git_parse_ctx_init(&ctx, buf, len);
242

243 244 245 246 247 248 249
	/* 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);
250
			continue; /* TODO: warn */
251
		}
252

253 254 255 256
		/* 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);
257
		if (error < 0)
258
			goto cleanup;
259 260

		error = 0;
261
	}
262

263
cleanup:
264 265 266 267
	git_buf_dispose(&real_name);
	git_buf_dispose(&real_email);
	git_buf_dispose(&replace_name);
	git_buf_dispose(&replace_email);
268 269
	return error;
}
270

271
int git_mailmap_from_buffer(git_mailmap **out, const char *data, size_t len)
272
{
273 274 275
	int error = git_mailmap_new(out);
	if (error < 0)
		return error;
276

277
	error = mailmap_add_buffer(*out, data, len);
278 279 280
	if (error < 0) {
		git_mailmap_free(*out);
		*out = NULL;
281
	}
282
	return error;
283
}
284

285
static int mailmap_add_blob(
286
	git_mailmap *mm, git_repository *repo, const char *rev)
287
{
288
	git_object *object = NULL;
289
	git_blob *blob = NULL;
290
	git_buf content = GIT_BUF_INIT;
291
	int error;
292

293
	assert(mm && repo);
294

295
	error = git_revparse_single(&object, repo, rev);
296 297 298
	if (error < 0)
		goto cleanup;

299
	error = git_object_peel((git_object **)&blob, object, GIT_OBJECT_BLOB);
300 301
	if (error < 0)
		goto cleanup;
302

303
	error = git_blob__getbuf(&content, blob);
304 305
	if (error < 0)
		goto cleanup;
306

307
	error = mailmap_add_buffer(mm, content.ptr, content.size);
308 309
	if (error < 0)
		goto cleanup;
310

311
cleanup:
312
	git_buf_dispose(&content);
313
	git_blob_free(blob);
314
	git_object_free(object);
315
	return error;
316 317
}

318 319
static int mailmap_add_file_ondisk(
	git_mailmap *mm, const char *path, git_repository *repo)
320
{
321 322 323
	const char *base = repo ? git_repository_workdir(repo) : NULL;
	git_buf fullpath = GIT_BUF_INIT;
	git_buf content = GIT_BUF_INIT;
324
	int error;
325

326
	error = git_path_join_unrooted(&fullpath, path, base, NULL);
327 328 329
	if (error < 0)
		goto cleanup;

330
	error = git_futils_readbuffer(&content, fullpath.ptr);
331 332 333
	if (error < 0)
		goto cleanup;

334
	error = mailmap_add_buffer(mm, content.ptr, content.size);
335 336
	if (error < 0)
		goto cleanup;
337 338

cleanup:
339 340
	git_buf_dispose(&fullpath);
	git_buf_dispose(&content);
341
	return error;
342
}
343

344 345
/* NOTE: Only expose with an error return, currently never errors */
static void mailmap_add_from_repository(git_mailmap *mm, git_repository *repo)
346
{
347
	git_config *config = NULL;
348
	git_buf rev_buf = GIT_BUF_INIT;
349
	git_buf path_buf = GIT_BUF_INIT;
350
	const char *rev = NULL;
351 352 353 354 355 356
	const char *path = NULL;

	assert(mm && repo);

	/* If we're in a bare repo, default blob to 'HEAD:.mailmap' */
	if (repo->is_bare)
357
		rev = MM_BLOB_DEFAULT;
358 359 360

	/* Try to load 'mailmap.file' and 'mailmap.blob' cfgs from the repo */
	if (git_repository_config(&config, repo) == 0) {
361 362
		if (git_config_get_string_buf(&rev_buf, config, MM_BLOB_CONFIG) == 0)
			rev = rev_buf.ptr;
363 364 365
		if (git_config_get_path(&path_buf, config, MM_FILE_CONFIG) == 0)
			path = path_buf.ptr;
	}
366

367 368 369 370 371 372 373 374 375 376 377 378 379
	/*
	 * 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);
380 381
	if (rev != NULL)
		mailmap_add_blob(mm, repo, rev);
382 383 384
	if (path != NULL)
		mailmap_add_file_ondisk(mm, path, repo);

385 386
	git_buf_dispose(&rev_buf);
	git_buf_dispose(&path_buf);
387 388
	git_config_free(config);
}
389

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
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;
406 407 408 409

	/* The lookup needle we want to use only sets the replace_email. */
	git_mailmap_entry needle = { NULL };
	needle.replace_email = (char *)email;
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461

	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;
462
}
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485

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;
}