1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
6 */
7
8 #include "mailmap.h"
9
10 #include "common.h"
11 #include "path.h"
12 #include "repository.h"
13 #include "signature.h"
14 #include "git2/config.h"
15 #include "git2/revparse.h"
16 #include "blob.h"
17 #include "parse.h"
18
19 #define MM_FILE ".mailmap"
20 #define MM_FILE_CONFIG "mailmap.file"
21 #define MM_BLOB_CONFIG "mailmap.blob"
22 #define MM_BLOB_DEFAULT "HEAD:" MM_FILE
23
mailmap_entry_free(git_mailmap_entry * entry)24 static void mailmap_entry_free(git_mailmap_entry *entry)
25 {
26 if (!entry)
27 return;
28
29 git__free(entry->real_name);
30 git__free(entry->real_email);
31 git__free(entry->replace_name);
32 git__free(entry->replace_email);
33 git__free(entry);
34 }
35
36 /*
37 * First we sort by replace_email, then replace_name (if present).
38 * Entries with names are greater than entries without.
39 */
mailmap_entry_cmp(const void * a_raw,const void * b_raw)40 static int mailmap_entry_cmp(const void *a_raw, const void *b_raw)
41 {
42 const git_mailmap_entry *a = (const git_mailmap_entry *)a_raw;
43 const git_mailmap_entry *b = (const git_mailmap_entry *)b_raw;
44 int cmp;
45
46 assert(a && b && a->replace_email && b->replace_email);
47
48 cmp = git__strcmp(a->replace_email, b->replace_email);
49 if (cmp)
50 return cmp;
51
52 /* NULL replace_names are less than not-NULL ones */
53 if (a->replace_name == NULL || b->replace_name == NULL)
54 return (int)(a->replace_name != NULL) - (int)(b->replace_name != NULL);
55
56 return git__strcmp(a->replace_name, b->replace_name);
57 }
58
59 /* Replace the old entry with the new on duplicate. */
mailmap_entry_replace(void ** old_raw,void * new_raw)60 static int mailmap_entry_replace(void **old_raw, void *new_raw)
61 {
62 mailmap_entry_free((git_mailmap_entry *)*old_raw);
63 *old_raw = new_raw;
64 return GIT_EEXISTS;
65 }
66
67 /* Check if we're at the end of line, w/ comments */
is_eol(git_parse_ctx * ctx)68 static bool is_eol(git_parse_ctx *ctx)
69 {
70 char c;
71 return git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE) < 0 || c == '#';
72 }
73
advance_until(const char ** start,size_t * len,git_parse_ctx * ctx,char needle)74 static int advance_until(
75 const char **start, size_t *len, git_parse_ctx *ctx, char needle)
76 {
77 *start = ctx->line;
78 while (ctx->line_len > 0 && *ctx->line != '#' && *ctx->line != needle)
79 git_parse_advance_chars(ctx, 1);
80
81 if (ctx->line_len == 0 || *ctx->line == '#')
82 return -1; /* end of line */
83
84 *len = ctx->line - *start;
85 git_parse_advance_chars(ctx, 1); /* advance past needle */
86 return 0;
87 }
88
89 /*
90 * Parse a single entry from a mailmap file.
91 *
92 * The output git_bufs will be non-owning, and should be copied before being
93 * persisted.
94 */
parse_mailmap_entry(git_buf * real_name,git_buf * real_email,git_buf * replace_name,git_buf * replace_email,git_parse_ctx * ctx)95 static int parse_mailmap_entry(
96 git_buf *real_name, git_buf *real_email,
97 git_buf *replace_name, git_buf *replace_email,
98 git_parse_ctx *ctx)
99 {
100 const char *start;
101 size_t len;
102
103 git_buf_clear(real_name);
104 git_buf_clear(real_email);
105 git_buf_clear(replace_name);
106 git_buf_clear(replace_email);
107
108 git_parse_advance_ws(ctx);
109 if (is_eol(ctx))
110 return -1; /* blank line */
111
112 /* Parse the real name */
113 if (advance_until(&start, &len, ctx, '<') < 0)
114 return -1;
115
116 git_buf_attach_notowned(real_name, start, len);
117 git_buf_rtrim(real_name);
118
119 /*
120 * If this is the last email in the line, this is the email to replace,
121 * otherwise, it's the real email.
122 */
123 if (advance_until(&start, &len, ctx, '>') < 0)
124 return -1;
125
126 /* If we aren't at the end of the line, parse a second name and email */
127 if (!is_eol(ctx)) {
128 git_buf_attach_notowned(real_email, start, len);
129
130 git_parse_advance_ws(ctx);
131 if (advance_until(&start, &len, ctx, '<') < 0)
132 return -1;
133 git_buf_attach_notowned(replace_name, start, len);
134 git_buf_rtrim(replace_name);
135
136 if (advance_until(&start, &len, ctx, '>') < 0)
137 return -1;
138 }
139
140 git_buf_attach_notowned(replace_email, start, len);
141
142 if (!is_eol(ctx))
143 return -1;
144
145 return 0;
146 }
147
git_mailmap_new(git_mailmap ** out)148 int git_mailmap_new(git_mailmap **out)
149 {
150 int error;
151 git_mailmap *mm = git__calloc(1, sizeof(git_mailmap));
152 GIT_ERROR_CHECK_ALLOC(mm);
153
154 error = git_vector_init(&mm->entries, 0, mailmap_entry_cmp);
155 if (error < 0) {
156 git__free(mm);
157 return error;
158 }
159 *out = mm;
160 return 0;
161 }
162
git_mailmap_free(git_mailmap * mm)163 void git_mailmap_free(git_mailmap *mm)
164 {
165 size_t idx;
166 git_mailmap_entry *entry;
167 if (!mm)
168 return;
169
170 git_vector_foreach(&mm->entries, idx, entry)
171 mailmap_entry_free(entry);
172
173 git_vector_free(&mm->entries);
174 git__free(mm);
175 }
176
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)177 static int mailmap_add_entry_unterminated(
178 git_mailmap *mm,
179 const char *real_name, size_t real_name_size,
180 const char *real_email, size_t real_email_size,
181 const char *replace_name, size_t replace_name_size,
182 const char *replace_email, size_t replace_email_size)
183 {
184 int error;
185 git_mailmap_entry *entry = git__calloc(1, sizeof(git_mailmap_entry));
186 GIT_ERROR_CHECK_ALLOC(entry);
187
188 assert(mm && replace_email && *replace_email);
189
190 if (real_name_size > 0) {
191 entry->real_name = git__substrdup(real_name, real_name_size);
192 GIT_ERROR_CHECK_ALLOC(entry->real_name);
193 }
194 if (real_email_size > 0) {
195 entry->real_email = git__substrdup(real_email, real_email_size);
196 GIT_ERROR_CHECK_ALLOC(entry->real_email);
197 }
198 if (replace_name_size > 0) {
199 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 if (error == GIT_EEXISTS)
207 error = GIT_OK;
208 else if (error < 0)
209 mailmap_entry_free(entry);
210
211 return error;
212 }
213
git_mailmap_add_entry(git_mailmap * mm,const char * real_name,const char * real_email,const char * replace_name,const char * replace_email)214 int git_mailmap_add_entry(
215 git_mailmap *mm, const char *real_name, const char *real_email,
216 const char *replace_name, const char *replace_email)
217 {
218 return mailmap_add_entry_unterminated(
219 mm,
220 real_name, real_name ? strlen(real_name) : 0,
221 real_email, real_email ? strlen(real_email) : 0,
222 replace_name, replace_name ? strlen(replace_name) : 0,
223 replace_email, strlen(replace_email));
224 }
225
mailmap_add_buffer(git_mailmap * mm,const char * buf,size_t len)226 static int mailmap_add_buffer(git_mailmap *mm, const char *buf, size_t len)
227 {
228 int error = 0;
229 git_parse_ctx ctx;
230
231 /* Scratch buffers containing the real parsed names & emails */
232 git_buf real_name = GIT_BUF_INIT;
233 git_buf real_email = GIT_BUF_INIT;
234 git_buf replace_name = GIT_BUF_INIT;
235 git_buf replace_email = GIT_BUF_INIT;
236
237 /* Buffers may not contain '\0's. */
238 if (memchr(buf, '\0', len) != NULL)
239 return -1;
240
241 git_parse_ctx_init(&ctx, buf, len);
242
243 /* Run the parser */
244 while (ctx.remain_len > 0) {
245 error = parse_mailmap_entry(
246 &real_name, &real_email, &replace_name, &replace_email, &ctx);
247 if (error < 0) {
248 error = 0; /* Skip lines which don't contain a valid entry */
249 git_parse_advance_line(&ctx);
250 continue; /* TODO: warn */
251 }
252
253 /* NOTE: Can't use add_entry(...) as our buffers aren't terminated */
254 error = mailmap_add_entry_unterminated(
255 mm, real_name.ptr, real_name.size, real_email.ptr, real_email.size,
256 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 git_buf_dispose(&real_name);
265 git_buf_dispose(&real_email);
266 git_buf_dispose(&replace_name);
267 git_buf_dispose(&replace_email);
268 return error;
269 }
270
git_mailmap_from_buffer(git_mailmap ** out,const char * data,size_t len)271 int git_mailmap_from_buffer(git_mailmap **out, const char *data, size_t len)
272 {
273 int error = git_mailmap_new(out);
274 if (error < 0)
275 return error;
276
277 error = mailmap_add_buffer(*out, data, len);
278 if (error < 0) {
279 git_mailmap_free(*out);
280 *out = NULL;
281 }
282 return error;
283 }
284
mailmap_add_blob(git_mailmap * mm,git_repository * repo,const char * rev)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 if (error < 0)
297 goto cleanup;
298
299 error = git_object_peel((git_object **)&blob, object, GIT_OBJECT_BLOB);
300 if (error < 0)
301 goto cleanup;
302
303 error = git_blob__getbuf(&content, blob);
304 if (error < 0)
305 goto cleanup;
306
307 error = mailmap_add_buffer(mm, content.ptr, content.size);
308 if (error < 0)
309 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
mailmap_add_file_ondisk(git_mailmap * mm,const char * path,git_repository * repo)318 static int mailmap_add_file_ondisk(
319 git_mailmap *mm, const char *path, git_repository *repo)
320 {
321 const char *base = repo ? git_repository_workdir(repo) : NULL;
322 git_buf fullpath = GIT_BUF_INIT;
323 git_buf content = GIT_BUF_INIT;
324 int error;
325
326 error = git_path_join_unrooted(&fullpath, path, base, NULL);
327 if (error < 0)
328 goto cleanup;
329
330 error = git_futils_readbuffer(&content, fullpath.ptr);
331 if (error < 0)
332 goto cleanup;
333
334 error = mailmap_add_buffer(mm, content.ptr, content.size);
335 if (error < 0)
336 goto cleanup;
337
338 cleanup:
339 git_buf_dispose(&fullpath);
340 git_buf_dispose(&content);
341 return error;
342 }
343
344 /* NOTE: Only expose with an error return, currently never errors */
mailmap_add_from_repository(git_mailmap * mm,git_repository * repo)345 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 const char *path = NULL;
352
353 assert(mm && repo);
354
355 /* If we're in a bare repo, default blob to 'HEAD:.mailmap' */
356 if (repo->is_bare)
357 rev = MM_BLOB_DEFAULT;
358
359 /* Try to load 'mailmap.file' and 'mailmap.blob' cfgs from the repo */
360 if (git_repository_config(&config, repo) == 0) {
361 if (git_config_get_string_buf(&rev_buf, config, MM_BLOB_CONFIG) == 0)
362 rev = rev_buf.ptr;
363 if (git_config_get_path(&path_buf, config, MM_FILE_CONFIG) == 0)
364 path = path_buf.ptr;
365 }
366
367 /*
368 * Load mailmap files in order, overriding previous entries with new ones.
369 * 1. The '.mailmap' file in the repository's workdir root,
370 * 2. The blob described by the 'mailmap.blob' config (default HEAD:.mailmap),
371 * 3. The file described by the 'mailmap.file' config.
372 *
373 * We ignore errors from these loads, as these files may not exist, or may
374 * contain invalid information, and we don't want to report that error.
375 *
376 * XXX: Warn?
377 */
378 if (!repo->is_bare)
379 mailmap_add_file_ondisk(mm, MM_FILE, repo);
380 if (rev != NULL)
381 mailmap_add_blob(mm, repo, rev);
382 if (path != NULL)
383 mailmap_add_file_ondisk(mm, path, repo);
384
385 git_buf_dispose(&rev_buf);
386 git_buf_dispose(&path_buf);
387 git_config_free(config);
388 }
389
git_mailmap_from_repository(git_mailmap ** out,git_repository * repo)390 int git_mailmap_from_repository(git_mailmap **out, git_repository *repo)
391 {
392 int error = git_mailmap_new(out);
393 if (error < 0)
394 return error;
395 mailmap_add_from_repository(*out, repo);
396 return 0;
397 }
398
git_mailmap_entry_lookup(const git_mailmap * mm,const char * name,const char * email)399 const git_mailmap_entry *git_mailmap_entry_lookup(
400 const git_mailmap *mm, const char *name, const char *email)
401 {
402 int error;
403 ssize_t fallback = -1;
404 size_t idx;
405 git_mailmap_entry *entry;
406
407 /* The lookup needle we want to use only sets the replace_email. */
408 git_mailmap_entry needle = { NULL };
409 needle.replace_email = (char *)email;
410
411 assert(email);
412
413 if (!mm)
414 return NULL;
415
416 /*
417 * We want to find the place to start looking. so we do a binary search for
418 * the "fallback" nameless entry. If we find it, we advance past it and record
419 * the index.
420 */
421 error = git_vector_bsearch(&idx, (git_vector *)&mm->entries, &needle);
422 if (error >= 0)
423 fallback = idx++;
424 else if (error != GIT_ENOTFOUND)
425 return NULL;
426
427 /* do a linear search for an exact match */
428 for (; idx < git_vector_length(&mm->entries); ++idx) {
429 entry = git_vector_get(&mm->entries, idx);
430
431 if (git__strcmp(entry->replace_email, email))
432 break; /* it's a different email, so we're done looking */
433
434 assert(entry->replace_name); /* should be specific */
435 if (!name || !git__strcmp(entry->replace_name, name))
436 return entry;
437 }
438
439 if (fallback < 0)
440 return NULL; /* no fallback */
441 return git_vector_get(&mm->entries, fallback);
442 }
443
git_mailmap_resolve(const char ** real_name,const char ** real_email,const git_mailmap * mailmap,const char * name,const char * email)444 int git_mailmap_resolve(
445 const char **real_name, const char **real_email,
446 const git_mailmap *mailmap,
447 const char *name, const char *email)
448 {
449 const git_mailmap_entry *entry = NULL;
450 assert(name && email);
451
452 *real_name = name;
453 *real_email = email;
454
455 if ((entry = git_mailmap_entry_lookup(mailmap, name, email))) {
456 if (entry->real_name)
457 *real_name = entry->real_name;
458 if (entry->real_email)
459 *real_email = entry->real_email;
460 }
461 return 0;
462 }
463
git_mailmap_resolve_signature(git_signature ** out,const git_mailmap * mailmap,const git_signature * sig)464 int git_mailmap_resolve_signature(
465 git_signature **out, const git_mailmap *mailmap, const git_signature *sig)
466 {
467 const char *name = NULL;
468 const char *email = NULL;
469 int error;
470
471 if (!sig)
472 return 0;
473
474 error = git_mailmap_resolve(&name, &email, mailmap, sig->name, sig->email);
475 if (error < 0)
476 return error;
477
478 error = git_signature_new(out, name, email, sig->when.time, sig->when.offset);
479 if (error < 0)
480 return error;
481
482 /* Copy over the sign, as git_signature_new doesn't let you pass it. */
483 (*out)->when.sign = sig->when.sign;
484 return 0;
485 }
486