Commit 8ff0504dd38b804865342d1be2f9d27f6f5d94f0

Nika Layzell 2018-04-08T03:01:14

mailmap: Rewrite API to support accurate mailmap resolution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
diff --git a/include/git2/mailmap.h b/include/git2/mailmap.h
index a8c4ccb..761db67 100644
--- a/include/git2/mailmap.h
+++ b/include/git2/mailmap.h
@@ -21,103 +21,93 @@
 GIT_BEGIN_DECL
 
 /**
- * A single entry parsed from a mailmap.
- */
-typedef struct git_mailmap_entry {
-	unsigned int version;
-
-	const char *real_name; /**< the real name (may be NULL) */
-	const char *real_email; /**< the real email (may be NULL) */
-	const char *replace_name; /**< the name to replace (may be NULL) */
-	const char *replace_email; /**< the email to replace */
-} git_mailmap_entry;
-
-#define GIT_MAILMAP_ENTRY_VERSION 1
-#define GIT_MAILMAP_ENTRY_INIT {GIT_MAILMAP_ENTRY_VERSION}
-
-/**
- * Create a mailmap object by parsing a mailmap file. Malformed entries in the
- * mailmap are ignored.
+ * Allocate a new mailmap object.
  *
- * The mailmap must be freed with 'git_mailmap_free'.
+ * This object is empty, so you'll have to add a mailmap file before you can do
+ * anything with it. The mailmap must be freed with 'git_mailmap_free'.
  *
- * @param out pointer to store the mailmap
- * @param buffer buffer to parse the mailmap from
- * @return 0 on success, otherwise an error code
+ * @param out pointer to store the new mailmap
+ * @return 0 on success, or an error code
  */
-GIT_EXTERN(int) git_mailmap_from_buffer(git_mailmap **out, git_buf *buffer);
+GIT_EXTERN(int) git_mailmap_new(git_mailmap **out);
 
 /**
- * Create a mailmap object from the given repository. Malformed entries in the
- * mailmap are ignored.
- *
- * If the repository is not bare, the repository's working directory root will
- * be checked for the '.mailmap' file to be parsed.
- *
- * If the repository is bare, the repository's HEAD commit's tree root will be
- * searched for the '.mailmap' file to be parsed.
- *
- * The mailmap must be freed with 'git_mailmap_free'.
+ * Free the mailmap and its associated memory.
  *
- * @param out pointer to store the mailmap
- * @param repo repository to find the .mailmap in
- * @return 0 on success; GIT_ENOTFOUND if .mailmap could not be found,
- *         otherwise an error code.
+ * @param mm the mailmap to free
  */
-GIT_EXTERN(int) git_mailmap_from_repo(git_mailmap **out, git_repository *repo);
+GIT_EXTERN(void) git_mailmap_free(git_mailmap *mm);
 
 /**
- * Free a mailmap created by 'git_mailmap_from_buffer' or
- * 'git_mailmap_from_repo'.
+ * Add a single entry to the given mailmap object. If the entry already exists,
+ * it will be replaced with the new entry.
+ *
+ * @param mm mailmap to add the entry to
+ * @param real_name the real name to use, or NULL
+ * @param real_email the real email to use, or NULL
+ * @param replace_name the name to replace, or NULL
+ * @param replace_email the email to replace
+ * @return 0 if it was added, EEXISTS if it replaced an entry, or an error code
  */
-GIT_EXTERN(void) git_mailmap_free(git_mailmap *mailmap);
+GIT_EXTERN(int) git_mailmap_add_entry(
+	git_mailmap *mm, const char *real_name, const char *real_email,
+	const char *replace_name, const char *replace_email);
 
 /**
- * Resolve a name and email to the corresponding real name and email.
+ * Parse mailmap entries from a buffer.
  *
- * The lifetime of the string results is tied to the `mailmap`, `name`, and
- * `email` parameters.
- *
- * @param name_out either 'name', or the real name to use.
- *             You should NOT free this value.
- * @param email_out either 'email' or the real email to use,
- *             You should NOT free this value.
- * @param mailmap the mailmap to perform the lookup in. (may be NULL)
- * @param name the name to resolve.
- * @param email the email to resolve.
- * @return 0 on success, otherwise an error code.
+ * @param mm mailmap to add the entries to
+ * @param buf the buffer to read the mailmap file from
+ * @return 0 on success, or an error code
  */
-GIT_EXTERN(int) git_mailmap_resolve(
-	const char **name_out, const char **email_out,
-	const git_mailmap *mailmap, const char *name, const char *email);
+GIT_EXTERN(int) git_mailmap_add_buffer(git_mailmap *mm, const git_buf *buf);
 
 /**
- * Get the number of mailmap entries in this mailmap.
+ * Create a new mailmap instance containing a single mailmap file
+ *
+ * This method is a simple utility wrapper for the following sequence
+ * of calls:
+ *  - git_mailmap_new
+ *  - git_mailmap_add_buffer
+ *
+ * @param out pointer to store the new mailmap
+ * @param buf buffer to parse the mailmap from
+ * @return 0 on success, or an error code
  */
-GIT_EXTERN(size_t) git_mailmap_entry_count(const git_mailmap *mailmap);
+GIT_EXTERN(int) git_mailmap_from_buffer(git_mailmap **out, const git_buf *buf);
 
 /**
- * Lookup a mailmap entry by index.
+ * Create a new mailmap instance from a repository, loading mailmap files based
+ * on the repository's configuration.
  *
- * Do not free the mailmap entry, it is owned by the mailmap.
+ * Mailmaps are loaded in the following order:
+ *  1. '.mailmap' in the root of the repository's working directory, if present.
+ *  2. The blob object identified by the 'mailmap.blob' config entry, if set.
+ * 	   [NOTE: 'mailmap.blob' defaults to 'HEAD:.mailmap' in bare repositories]
+ *  3. The path in the 'mailmap.file' config entry, if set.
  *
- * @return the mailmap entry at index, or NULL if it cannot be found.
+ * @param out pointer to store the new mailmap
+ * @param repo repository to load mailmap information from
+ * @return 0 on success, or an error code
  */
-GIT_EXTERN(const git_mailmap_entry *) git_mailmap_entry_byindex(
-	const git_mailmap *mailmap, size_t idx);
+GIT_EXTERN(int) git_mailmap_from_repository(
+	git_mailmap **out, git_repository *repo);
 
 /**
- * Lookup a mailmap entry by name/email pair.
+ * Resolve a name and email to the corresponding real name and email.
  *
- * Do not free the mailmap entry, it is owned by the mailmap.
+ * The lifetime of the strings are tied to `mm`, `name`, and `email` parameters.
  *
- * @param mailmap the mailmap to perform the lookup in. (may be NULL)
- * @param name the name to perform the lookup with.
- * @param email the email to perform the lookup with.
- * @return the corresponding mailmap entry, or NULL if it cannot be found.
+ * @param real_name pointer to store the real name
+ * @param real_email pointer to store the real email
+ * @param mm the mailmap to perform a lookup with (may be NULL)
+ * @param name the name to look up
+ * @param email the email to look up
+ * @return 0 on success, or an error code
  */
-GIT_EXTERN(const git_mailmap_entry *) git_mailmap_entry_lookup(
-	const git_mailmap *mailmap, const char *name, const char *email);
+GIT_EXTERN(int) git_mailmap_resolve(
+	const char **real_name, const char **real_email,
+	const git_mailmap *mm, const char *name, const char *email);
 
 /** @} */
 GIT_END_DECL
diff --git a/src/blame.c b/src/blame.c
index 0624bf4..fc87bd1 100644
--- a/src/blame.c
+++ b/src/blame.c
@@ -134,7 +134,7 @@ git_blame* git_blame__alloc(
 	}
 
 	if (opts.flags & GIT_BLAME_USE_MAILMAP)
-		git_mailmap_from_repo(&gbr->mailmap, repo);
+		git_mailmap_from_repository(&gbr->mailmap, repo);
 
 	return gbr;
 }
diff --git a/src/mailmap.c b/src/mailmap.c
index a178c31..3da46a8 100644
--- a/src/mailmap.c
+++ b/src/mailmap.c
@@ -5,21 +5,20 @@
  * a Linking Exception. For full terms see the included COPYING file.
  */
 
-#include "git2/mailmap.h"
+#include "mailmap.h"
 
+#include "common.h"
+#include "path.h"
+#include "repository.h"
+#include "git2/config.h"
+#include "git2/revparse.h"
 #include "blob.h"
-#include "commit.h"
 #include "parse.h"
-#include "git2/common.h"
-#include "git2/repository.h"
-#include "git2/revparse.h"
-#include "git2/sys/commit.h"
-
-#define MAILMAP_FILE ".mailmap"
 
-struct git_mailmap {
-	git_vector entries;
-};
+#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)
 {
@@ -33,6 +32,37 @@ static void mailmap_entry_free(git_mailmap_entry *entry)
 	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)
 {
@@ -114,12 +144,68 @@ static int parse_mailmap_entry(
 	return 0;
 }
 
-int git_mailmap_from_buffer(git_mailmap **out, git_buf *buf)
+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__free(mm);
+}
+
+int git_mailmap_add_entry(
+	git_mailmap *mm, const char *real_name, const char *real_email,
+	const char *replace_name, const char *replace_email)
+{
+	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 && *real_name) {
+		entry->real_name = git__strdup(real_name);
+		GITERR_CHECK_ALLOC(entry->real_name);
+	}
+	if (real_email && *real_email) {
+		entry->real_email = git__strdup(real_email);
+		GITERR_CHECK_ALLOC(entry->real_email);
+	}
+	if (replace_name && *replace_name) {
+		entry->replace_name = git__strdup(replace_name);
+		GITERR_CHECK_ALLOC(entry->replace_name);
+	}
+	entry->replace_email = git__strdup(replace_email);
+	GITERR_CHECK_ALLOC(entry->replace_email);
+
+	error = git_vector_insert_sorted(&mm->entries, entry, mailmap_entry_replace);
+	if (error < 0 && error != GIT_EEXISTS)
+		mailmap_entry_free(entry);
+
+	return error;
+}
+
+int git_mailmap_add_buffer(git_mailmap *mm, const git_buf *buf)
 {
 	int error;
-	git_mailmap *mm;
-	size_t entry_size;
-	char *entry_data;
 	git_mailmap_entry *entry = NULL;
 	git_parse_ctx ctx;
 
@@ -134,14 +220,6 @@ int git_mailmap_from_buffer(git_mailmap **out, git_buf *buf)
 
 	git_parse_ctx_init(&ctx, buf->ptr, buf->size);
 
-	/* Create our mailmap object */
-	mm = git__calloc(1, sizeof(git_mailmap));
-	GITERR_CHECK_ALLOC(mm);
-
-	error = git_vector_init(&mm->entries, 0, NULL);
-	if (error < 0)
-		goto cleanup;
-
 	/* Run the parser */
 	while (ctx.remain_len > 0) {
 		error = parse_mailmap_entry(
@@ -154,7 +232,6 @@ int git_mailmap_from_buffer(git_mailmap **out, git_buf *buf)
 
 		entry = git__calloc(1, sizeof(git_mailmap_entry));
 		GITERR_CHECK_ALLOC(entry);
-		entry->version = GIT_MAILMAP_ENTRY_VERSION;
 
 		if (real_name.size > 0) {
 			entry->real_name = git__substrdup(real_name.ptr, real_name.size);
@@ -171,19 +248,17 @@ int git_mailmap_from_buffer(git_mailmap **out, git_buf *buf)
 		entry->replace_email = git__substrdup(replace_email.ptr, replace_email.size);
 		GITERR_CHECK_ALLOC(entry->replace_email);
 
-		error = git_vector_insert(&mm->entries, entry);
-		if (error < 0)
+		error = git_vector_insert_sorted(
+			&mm->entries, entry, mailmap_entry_replace);
+		if (error < 0 && error != GIT_EEXISTS)
 			goto cleanup;
+
 		entry = NULL;
+		error = 0;
 	}
 
-	/* fill in *out, and make sure we don't free our mailmap */
-	*out = mm;
-	mm = NULL;
-
 cleanup:
 	mailmap_entry_free(entry);
-	git_mailmap_free(mm);
 
 	/* We never allocate data in these buffers, but better safe than sorry */
 	git_buf_free(&real_name);
@@ -193,157 +268,192 @@ cleanup:
 	return error;
 }
 
-void git_mailmap_free(git_mailmap *mailmap)
-{
-	if (!mailmap)
-		return;
-
-	git_vector_foreach(&mailmap->entries, i, entry) {
-		mailmap_entry_free(entry);
-	}
-	git_vector_free(&mailmap->entries);
-
-	git__free(mailmap);
-}
-
-int git_mailmap_resolve(
-	const char **name_out, const char **email_out,
-	const git_mailmap *mailmap,
-	const char *name, const char *email)
-{
-	const git_mailmap_entry *entry = NULL;
-	assert(name && email);
-
-	*name_out = name;
-	*email_out = email;
-
-	if (!mailmap)
-		return 0;
-
-	entry = git_mailmap_entry_lookup(mailmap, name, email);
-	if (entry) {
-		if (entry->real_name)
-			*name_out = entry->real_name;
-		if (entry->real_email)
-			*email_out = entry->real_email;
-	}
-	return 0;
-}
-
-const git_mailmap_entry *git_mailmap_entry_lookup(
-	const git_mailmap *mailmap, const char *name, const char *email)
+int git_mailmap_from_buffer(git_mailmap **out, const git_buf *buffer)
 {
-	size_t i;
-	git_mailmap_entry *entry;
-	assert(name && email);
-
-	if (!mailmap)
-		return NULL;
-
-	git_vector_foreach(&mailmap->entries, i, entry) {
-		if (git__strcmp(email, entry->replace_email))
-			continue;
-		if (entry->replace_name && git__strcmp(name, entry->replace_name))
-			continue;
+	int error = git_mailmap_new(out);
+	if (error < 0)
+		return error;
 
-		return entry;
+	error = git_mailmap_add_buffer(*out, buffer);
+	if (error < 0) {
+		git_mailmap_free(*out);
+		*out = NULL;
 	}
-
-	return NULL;
-}
-
-const git_mailmap_entry *git_mailmap_entry_byindex(
-	const git_mailmap *mailmap, size_t idx)
-{
-	if (mailmap)
-		return git_vector_get(&mailmap->entries, idx);
-	return NULL;
-}
-
-size_t git_mailmap_entry_count(const git_mailmap *mailmap)
-{
-	if (mailmap)
-		return git_vector_length(&mailmap->entries);
-	return 0;
+	return error;
 }
 
-static int mailmap_from_bare_repo(git_mailmap **mailmap, git_repository *repo)
+static int mailmap_add_blob(
+	git_mailmap *mm, git_repository *repo, const char *spec)
 {
-	git_reference *head = NULL;
-	git_object *tree = NULL;
+	git_object *object = NULL;
 	git_blob *blob = NULL;
 	git_buf content = GIT_BUF_INIT;
 	int error;
 
-	assert(git_repository_is_bare(repo));
+	assert(mm && repo);
 
-	/* In bare repositories, fall back to reading from HEAD's tree */
-	error = git_repository_head(&head, repo);
+	error = git_revparse_single(&object, repo, spec);
 	if (error < 0)
 		goto cleanup;
 
-	error = git_reference_peel(&tree, head, GIT_OBJ_TREE);
+	error = git_object_peel((git_object **)&blob, object, GIT_OBJ_BLOB);
 	if (error < 0)
 		goto cleanup;
 
-	error = git_object_lookup_bypath(
-		(git_object **) &blob, tree, MAILMAP_FILE, GIT_OBJ_BLOB);
+	error = git_blob__getbuf(&content, blob);
 	if (error < 0)
 		goto cleanup;
 
-	error = git_blob_filtered_content(&content, blob, MAILMAP_FILE, false);
-	if (error < 0)
-		goto cleanup;
-
-	error = git_mailmap_from_buffer(mailmap, &content);
+	error = git_mailmap_add_buffer(mm, &content);
 	if (error < 0)
 		goto cleanup;
 
 cleanup:
 	git_buf_free(&content);
 	git_blob_free(blob);
-	git_object_free(tree);
-	git_reference_free(head);
-
+	git_object_free(object);
 	return error;
 }
 
-static int mailmap_from_workdir_repo(git_mailmap **mailmap, git_repository *repo)
+static int mailmap_add_file_ondisk(
+	git_mailmap *mm, const char *path, git_repository *repo)
 {
-	git_buf path = GIT_BUF_INIT;
-	git_buf data = GIT_BUF_INIT;
+	const char *base = repo ? git_repository_workdir(repo) : NULL;
+	git_buf fullpath = GIT_BUF_INIT;
+	git_buf content = GIT_BUF_INIT;
 	int error;
 
-	assert(!git_repository_is_bare(repo));
-
-	/* In non-bare repositories, .mailmap should be read from the workdir */
-	error = git_buf_joinpath(&path, git_repository_workdir(repo), MAILMAP_FILE);
+	error = git_path_join_unrooted(&fullpath, path, base, NULL);
 	if (error < 0)
 		goto cleanup;
 
-	error = git_futils_readbuffer(&data, git_buf_cstr(&path));
+	error = git_futils_readbuffer(&content, fullpath.ptr);
 	if (error < 0)
 		goto cleanup;
 
-	error = git_mailmap_from_buffer(mailmap, &data);
+	error = git_mailmap_add_buffer(mm, &content);
 	if (error < 0)
 		goto cleanup;
 
 cleanup:
-	git_buf_free(&path);
-	git_buf_free(&data);
-
+	git_buf_free(&fullpath);
+	git_buf_free(&content);
 	return error;
 }
 
-int git_mailmap_from_repo(git_mailmap **mailmap, git_repository *repo)
+/* NOTE: Only expose with an error return, currently never errors */
+static void mailmap_add_from_repository(git_mailmap *mm, git_repository *repo)
 {
-	assert(mailmap && repo);
+	git_config *config = NULL;
+	git_buf spec_buf = GIT_BUF_INIT;
+	git_buf path_buf = GIT_BUF_INIT;
+	const char *spec = NULL;
+	const char *path = NULL;
+
+	assert(mm && repo);
+
+	/* If we're in a bare repo, default blob to 'HEAD:.mailmap' */
+	if (repo->is_bare)
+		spec = 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(&spec_buf, config, MM_BLOB_CONFIG) == 0)
+			spec = spec_buf.ptr;
+		if (git_config_get_path(&path_buf, config, MM_FILE_CONFIG) == 0)
+			path = path_buf.ptr;
+	}
 
-	*mailmap = NULL;
+	/*
+	 * 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 (spec != NULL)
+		mailmap_add_blob(mm, repo, spec);
+	if (path != NULL)
+		mailmap_add_file_ondisk(mm, path, repo);
+
+	git_buf_free(&spec_buf);
+	git_buf_free(&path_buf);
+	git_config_free(config);
+}
 
-	if (git_repository_is_bare(repo))
-		return mailmap_from_bare_repo(mailmap, repo);
-	else
-		return mailmap_from_workdir_repo(mailmap, repo);
+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;
+	git_mailmap_entry needle = { NULL, NULL, NULL, (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;
 }
diff --git a/src/mailmap.h b/src/mailmap.h
new file mode 100644
index 0000000..2c9736a
--- /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
diff --git a/src/signature.c b/src/signature.c
index d922fda..a8177e0 100644
--- a/src/signature.c
+++ b/src/signature.c
@@ -116,7 +116,8 @@ int git_signature_with_mailmap(
 	if (source == NULL)
 		return 0;
 
-	error = git_mailmap_resolve(&name, &email, mailmap, source->name, source->email);
+	error = git_mailmap_resolve(
+		&name, &email, mailmap, source->name, source->email);
 	if (error < 0)
 		return error;