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