mailmap.c 12.4 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
#include "common.h"
11
#include "config.h"
12
#include "fs_path.h"
13
#include "repository.h"
14
#include "signature.h"
15 16
#include "git2/config.h"
#include "git2/revparse.h"
17
#include "blob.h"
18
#include "parse.h"
19
#include "path.h"
20

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

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

38 39 40 41 42 43 44 45 46 47
/*
 * 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;

48 49
	GIT_ASSERT_ARG(a && a->replace_email);
	GIT_ASSERT_ARG(b && b->replace_email);
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

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

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

77 78 79 80 81 82 83 84 85 86 87 88 89
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;
90 91
}

92 93
/*
 * Parse a single entry from a mailmap file.
94
 *
95
 * The output git_strs will be non-owning, and should be copied before being
96
 * persisted.
97
 */
98
static int parse_mailmap_entry(
99 100
	git_str *real_name, git_str *real_email,
	git_str *replace_name, git_str *replace_email,
101
	git_parse_ctx *ctx)
102
{
103 104
	const char *start;
	size_t len;
105

106 107 108 109
	git_str_clear(real_name);
	git_str_clear(real_email);
	git_str_clear(replace_name);
	git_str_clear(replace_email);
110

111
	git_parse_advance_ws(ctx);
112 113 114 115
	if (is_eol(ctx))
		return -1; /* blank line */

	/* Parse the real name */
116 117
	if (advance_until(&start, &len, ctx, '<') < 0)
		return -1;
118

119 120
	git_str_attach_notowned(real_name, start, len);
	git_str_rtrim(real_name);
121

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

129 130
	/* If we aren't at the end of the line, parse a second name and email */
	if (!is_eol(ctx)) {
131
		git_str_attach_notowned(real_email, start, len);
132 133 134 135

		git_parse_advance_ws(ctx);
		if (advance_until(&start, &len, ctx, '<') < 0)
			return -1;
136 137
		git_str_attach_notowned(replace_name, start, len);
		git_str_rtrim(replace_name);
138 139 140

		if (advance_until(&start, &len, ctx, '>') < 0)
			return -1;
141
	}
142

143
	git_str_attach_notowned(replace_email, start, len);
144 145 146 147

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

148 149
	return 0;
}
150

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

	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);
175 176

	git_vector_free(&mm->entries);
177 178 179
	git__free(mm);
}

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

191 192
	GIT_ASSERT_ARG(mm);
	GIT_ASSERT_ARG(replace_email && *replace_email);
193

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

	error = git_vector_insert_sorted(&mm->entries, entry, mailmap_entry_replace);
210 211 212
	if (error == GIT_EEXISTS)
		error = GIT_OK;
	else if (error < 0)
213 214 215 216 217
		mailmap_entry_free(entry);

	return error;
}

218 219 220 221 222 223 224 225 226 227 228 229
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));
}

230
static int mailmap_add_buffer(git_mailmap *mm, const char *buf, size_t len)
231
{
232
	int error = 0;
233 234 235
	git_parse_ctx ctx;

	/* Scratch buffers containing the real parsed names & emails */
236 237 238 239
	git_str real_name = GIT_STR_INIT;
	git_str real_email = GIT_STR_INIT;
	git_str replace_name = GIT_STR_INIT;
	git_str replace_email = GIT_STR_INIT;
240

241 242
	/* Buffers may not contain '\0's. */
	if (memchr(buf, '\0', len) != NULL)
243
		return -1;
244

245
	git_parse_ctx_init(&ctx, buf, len);
246

247 248 249 250 251 252 253
	/* 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);
254
			continue; /* TODO: warn */
255
		}
256

257 258 259 260
		/* 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);
261
		if (error < 0)
262
			goto cleanup;
263 264

		error = 0;
265
	}
266

267
cleanup:
268 269 270 271
	git_str_dispose(&real_name);
	git_str_dispose(&real_email);
	git_str_dispose(&replace_name);
	git_str_dispose(&replace_email);
272 273
	return error;
}
274

275
int git_mailmap_from_buffer(git_mailmap **out, const char *data, size_t len)
276
{
277 278 279
	int error = git_mailmap_new(out);
	if (error < 0)
		return error;
280

281
	error = mailmap_add_buffer(*out, data, len);
282 283 284
	if (error < 0) {
		git_mailmap_free(*out);
		*out = NULL;
285
	}
286
	return error;
287
}
288

289
static int mailmap_add_blob(
290
	git_mailmap *mm, git_repository *repo, const char *rev)
291
{
292
	git_object *object = NULL;
293
	git_blob *blob = NULL;
294
	git_str content = GIT_STR_INIT;
295
	int error;
296

297 298
	GIT_ASSERT_ARG(mm);
	GIT_ASSERT_ARG(repo);
299

300
	error = git_revparse_single(&object, repo, rev);
301 302 303
	if (error < 0)
		goto cleanup;

304
	error = git_object_peel((git_object **)&blob, object, GIT_OBJECT_BLOB);
305 306
	if (error < 0)
		goto cleanup;
307

308
	error = git_blob__getbuf(&content, blob);
309 310
	if (error < 0)
		goto cleanup;
311

312
	error = mailmap_add_buffer(mm, content.ptr, content.size);
313 314
	if (error < 0)
		goto cleanup;
315

316
cleanup:
317
	git_str_dispose(&content);
318
	git_blob_free(blob);
319
	git_object_free(object);
320
	return error;
321 322
}

323 324
static int mailmap_add_file_ondisk(
	git_mailmap *mm, const char *path, git_repository *repo)
325
{
326
	const char *base = repo ? git_repository_workdir(repo) : NULL;
327 328
	git_str fullpath = GIT_STR_INIT;
	git_str content = GIT_STR_INIT;
329
	int error;
330

331
	error = git_fs_path_join_unrooted(&fullpath, path, base, NULL);
332 333 334
	if (error < 0)
		goto cleanup;

335
	error = git_path_validate_str_length(repo, &fullpath);
336 337 338
	if (error < 0)
		goto cleanup;

339
	error = git_futils_readbuffer(&content, fullpath.ptr);
340 341 342
	if (error < 0)
		goto cleanup;

343
	error = mailmap_add_buffer(mm, content.ptr, content.size);
344 345
	if (error < 0)
		goto cleanup;
346 347

cleanup:
348 349
	git_str_dispose(&fullpath);
	git_str_dispose(&content);
350
	return error;
351
}
352

353 354
/* NOTE: Only expose with an error return, currently never errors */
static void mailmap_add_from_repository(git_mailmap *mm, git_repository *repo)
355
{
356
	git_config *config = NULL;
357 358
	git_str rev_buf = GIT_STR_INIT;
	git_str path_buf = GIT_STR_INIT;
359
	const char *rev = NULL;
360 361 362 363
	const char *path = NULL;

	/* If we're in a bare repo, default blob to 'HEAD:.mailmap' */
	if (repo->is_bare)
364
		rev = MM_BLOB_DEFAULT;
365 366 367

	/* Try to load 'mailmap.file' and 'mailmap.blob' cfgs from the repo */
	if (git_repository_config(&config, repo) == 0) {
368
		if (git_config__get_string_buf(&rev_buf, config, MM_BLOB_CONFIG) == 0)
369
			rev = rev_buf.ptr;
370
		if (git_config__get_path(&path_buf, config, MM_FILE_CONFIG) == 0)
371 372
			path = path_buf.ptr;
	}
373

374 375 376 377 378 379 380 381 382 383 384 385 386
	/*
	 * 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);
387 388
	if (rev != NULL)
		mailmap_add_blob(mm, repo, rev);
389 390 391
	if (path != NULL)
		mailmap_add_file_ondisk(mm, path, repo);

392 393
	git_str_dispose(&rev_buf);
	git_str_dispose(&path_buf);
394 395
	git_config_free(config);
}
396

397 398
int git_mailmap_from_repository(git_mailmap **out, git_repository *repo)
{
399 400 401 402 403 404
	int error;

	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(repo);

	if ((error = git_mailmap_new(out)) < 0)
405
		return error;
406

407 408 409 410 411 412 413 414 415 416 417
	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;
418 419 420 421

	/* The lookup needle we want to use only sets the replace_email. */
	git_mailmap_entry needle = { NULL };
	needle.replace_email = (char *)email;
422

423
	GIT_ASSERT_ARG_WITH_RETVAL(email, NULL);
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445

	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 */

446 447
		 /* should be specific */
		GIT_ASSERT_WITH_RETVAL(entry->replace_name, NULL);
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
		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;
463 464 465

	GIT_ASSERT(name);
	GIT_ASSERT(email);
466 467 468 469 470 471 472 473 474 475 476

	*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;
477
}
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500

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