builtin-am: extract patch and commit info with git-mailinfo

For the purpose of applying the patch and committing the results, implement extracting the patch data, commit message and authorship from an e-mail message using git-mailinfo. git-mailinfo is run as a separate process, but ideally in the future, we should be be able to access its functionality directly without spawning a new process. Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Jeff King <peff@peff.net> Helped-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Paul Tan <pyokagan@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Paul Tan committed Aug 4, 2015 at 21:51 UTC 3e20dcf367f551e5a0efab74e7bc2c07f0549a3d
1 file changed +317
builtin/am.c
+317
@@ -9,6 +9,23 @@
9 #include "parse-options.h"
10 #include "dir.h"
11 #include "run-command.h"
12 +#include "quote.h"
13 +
14 +/**
15 + * Returns 1 if the file is empty or does not exist, 0 otherwise.
16 + */
17 +static int is_empty_file(const char *filename)
18 +{
19 + struct stat st;
20 +
21 + if (stat(filename, &st) < 0) {
22 + if (errno == ENOENT)
23 + return 1;
24 + die_errno(_("could not stat %s"), filename);
25 + }
26 +
27 + return !st.st_size;
28 +}
29
30 /**
31 * Like strbuf_getline(), but treats both '\n' and "\r\n" as line terminators.
@@ -38,6 +55,13 @@ struct am_state {
55 int cur;
56 int last;
57
58 + /* commit metadata and message */
59 + char *author_name;
60 + char *author_email;
61 + char *author_date;
62 + char *msg;
63 + size_t msg_len;
64 +
65 /* number of digits in patch filename */
66 int prec;
67 };
@@ -62,6 +86,10 @@ static void am_state_init(struct am_state *state, const char *dir)
86 static void am_state_release(struct am_state *state)
87 {
88 free(state->dir);
89 + free(state->author_name);
90 + free(state->author_email);
91 + free(state->author_date);
92 + free(state->msg);
93 }
94
95 /**
@@ -111,6 +139,161 @@ static int read_state_file(struct strbuf *sb, const struct am_state *state,
139 die_errno(_("could not read '%s'"), am_path(state, file));
140 }
141
142 +/**
143 + * Reads a KEY=VALUE shell variable assignment from `fp`, returning the VALUE
144 + * as a newly-allocated string. VALUE must be a quoted string, and the KEY must
145 + * match `key`. Returns NULL on failure.
146 + *
147 + * This is used by read_author_script() to read the GIT_AUTHOR_* variables from
148 + * the author-script.
149 + */
150 +static char *read_shell_var(FILE *fp, const char *key)
151 +{
152 + struct strbuf sb = STRBUF_INIT;
153 + const char *str;
154 +
155 + if (strbuf_getline(&sb, fp, '\n'))
156 + goto fail;
157 +
158 + if (!skip_prefix(sb.buf, key, &str))
159 + goto fail;
160 +
161 + if (!skip_prefix(str, "=", &str))
162 + goto fail;
163 +
164 + strbuf_remove(&sb, 0, str - sb.buf);
165 +
166 + str = sq_dequote(sb.buf);
167 + if (!str)
168 + goto fail;
169 +
170 + return strbuf_detach(&sb, NULL);
171 +
172 +fail:
173 + strbuf_release(&sb);
174 + return NULL;
175 +}
176 +
177 +/**
178 + * Reads and parses the state directory's "author-script" file, and sets
179 + * state->author_name, state->author_email and state->author_date accordingly.
180 + * Returns 0 on success, -1 if the file could not be parsed.
181 + *
182 + * The author script is of the format:
183 + *
184 + * GIT_AUTHOR_NAME='$author_name'
185 + * GIT_AUTHOR_EMAIL='$author_email'
186 + * GIT_AUTHOR_DATE='$author_date'
187 + *
188 + * where $author_name, $author_email and $author_date are quoted. We are strict
189 + * with our parsing, as the file was meant to be eval'd in the old git-am.sh
190 + * script, and thus if the file differs from what this function expects, it is
191 + * better to bail out than to do something that the user does not expect.
192 + */
193 +static int read_author_script(struct am_state *state)
194 +{
195 + const char *filename = am_path(state, "author-script");
196 + FILE *fp;
197 +
198 + assert(!state->author_name);
199 + assert(!state->author_email);
200 + assert(!state->author_date);
201 +
202 + fp = fopen(filename, "r");
203 + if (!fp) {
204 + if (errno == ENOENT)
205 + return 0;
206 + die_errno(_("could not open '%s' for reading"), filename);
207 + }
208 +
209 + state->author_name = read_shell_var(fp, "GIT_AUTHOR_NAME");
210 + if (!state->author_name) {
211 + fclose(fp);
212 + return -1;
213 + }
214 +
215 + state->author_email = read_shell_var(fp, "GIT_AUTHOR_EMAIL");
216 + if (!state->author_email) {
217 + fclose(fp);
218 + return -1;
219 + }
220 +
221 + state->author_date = read_shell_var(fp, "GIT_AUTHOR_DATE");
222 + if (!state->author_date) {
223 + fclose(fp);
224 + return -1;
225 + }
226 +
227 + if (fgetc(fp) != EOF) {
228 + fclose(fp);
229 + return -1;
230 + }
231 +
232 + fclose(fp);
233 + return 0;
234 +}
235 +
236 +/**
237 + * Saves state->author_name, state->author_email and state->author_date in the
238 + * state directory's "author-script" file.
239 + */
240 +static void write_author_script(const struct am_state *state)
241 +{
242 + struct strbuf sb = STRBUF_INIT;
243 +
244 + strbuf_addstr(&sb, "GIT_AUTHOR_NAME=");
245 + sq_quote_buf(&sb, state->author_name);
246 + strbuf_addch(&sb, '\n');
247 +
248 + strbuf_addstr(&sb, "GIT_AUTHOR_EMAIL=");
249 + sq_quote_buf(&sb, state->author_email);
250 + strbuf_addch(&sb, '\n');
251 +
252 + strbuf_addstr(&sb, "GIT_AUTHOR_DATE=");
253 + sq_quote_buf(&sb, state->author_date);
254 + strbuf_addch(&sb, '\n');
255 +
256 + write_file(am_path(state, "author-script"), 1, "%s", sb.buf);
257 +
258 + strbuf_release(&sb);
259 +}
260 +
261 +/**
262 + * Reads the commit message from the state directory's "final-commit" file,
263 + * setting state->msg to its contents and state->msg_len to the length of its
264 + * contents in bytes.
265 + *
266 + * Returns 0 on success, -1 if the file does not exist.
267 + */
268 +static int read_commit_msg(struct am_state *state)
269 +{
270 + struct strbuf sb = STRBUF_INIT;
271 +
272 + assert(!state->msg);
273 +
274 + if (read_state_file(&sb, state, "final-commit", 0) < 0) {
275 + strbuf_release(&sb);
276 + return -1;
277 + }
278 +
279 + state->msg = strbuf_detach(&sb, &state->msg_len);
280 + return 0;
281 +}
282 +
283 +/**
284 + * Saves state->msg in the state directory's "final-commit" file.
285 + */
286 +static void write_commit_msg(const struct am_state *state)
287 +{
288 + int fd;
289 + const char *filename = am_path(state, "final-commit");
290 +
291 + fd = xopen(filename, O_WRONLY | O_CREAT, 0666);
292 + if (write_in_full(fd, state->msg, state->msg_len) < 0)
293 + die_errno(_("could not write to %s"), filename);
294 + close(fd);
295 +}
296 +
297 /**
298 * Loads state from disk.
299 */
@@ -126,6 +309,11 @@ static void am_load(struct am_state *state)
309 die("BUG: state file 'last' does not exist");
310 state->last = strtol(sb.buf, NULL, 10);
311
312 + if (read_author_script(state) < 0)
313 + die(_("could not parse author script"));
314 +
315 + read_commit_msg(state);
316 +
317 strbuf_release(&sb);
318 }
319
@@ -318,19 +506,148 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,
506 */
507 static void am_next(struct am_state *state)
508 {
509 + free(state->author_name);
510 + state->author_name = NULL;
511 +
512 + free(state->author_email);
513 + state->author_email = NULL;
514 +
515 + free(state->author_date);
516 + state->author_date = NULL;
517 +
518 + free(state->msg);
519 + state->msg = NULL;
520 + state->msg_len = 0;
521 +
522 + unlink(am_path(state, "author-script"));
523 + unlink(am_path(state, "final-commit"));
524 +
525 state->cur++;
526 write_file(am_path(state, "next"), 1, "%d", state->cur);
527 }
528
529 +/**
530 + * Returns the filename of the current patch email.
531 + */
532 +static const char *msgnum(const struct am_state *state)
533 +{
534 + static struct strbuf sb = STRBUF_INIT;
535 +
536 + strbuf_reset(&sb);
537 + strbuf_addf(&sb, "%0*d", state->prec, state->cur);
538 +
539 + return sb.buf;
540 +}
541 +
542 +/**
543 + * Parses `mail` using git-mailinfo, extracting its patch and authorship info.
544 + * state->msg will be set to the patch message. state->author_name,
545 + * state->author_email and state->author_date will be set to the patch author's
546 + * name, email and date respectively. The patch body will be written to the
547 + * state directory's "patch" file.
548 + *
549 + * Returns 1 if the patch should be skipped, 0 otherwise.
550 + */
551 +static int parse_mail(struct am_state *state, const char *mail)
552 +{
553 + FILE *fp;
554 + struct child_process cp = CHILD_PROCESS_INIT;
555 + struct strbuf sb = STRBUF_INIT;
556 + struct strbuf msg = STRBUF_INIT;
557 + struct strbuf author_name = STRBUF_INIT;
558 + struct strbuf author_date = STRBUF_INIT;
559 + struct strbuf author_email = STRBUF_INIT;
560 + int ret = 0;
561 +
562 + cp.git_cmd = 1;
563 + cp.in = xopen(mail, O_RDONLY, 0);
564 + cp.out = xopen(am_path(state, "info"), O_WRONLY | O_CREAT, 0777);
565 +
566 + argv_array_push(&cp.args, "mailinfo");
567 + argv_array_push(&cp.args, am_path(state, "msg"));
568 + argv_array_push(&cp.args, am_path(state, "patch"));
569 +
570 + if (run_command(&cp) < 0)
571 + die("could not parse patch");
572 +
573 + close(cp.in);
574 + close(cp.out);
575 +
576 + /* Extract message and author information */
577 + fp = xfopen(am_path(state, "info"), "r");
578 + while (!strbuf_getline(&sb, fp, '\n')) {
579 + const char *x;
580 +
581 + if (skip_prefix(sb.buf, "Subject: ", &x)) {
582 + if (msg.len)
583 + strbuf_addch(&msg, '\n');
584 + strbuf_addstr(&msg, x);
585 + } else if (skip_prefix(sb.buf, "Author: ", &x))
586 + strbuf_addstr(&author_name, x);
587 + else if (skip_prefix(sb.buf, "Email: ", &x))
588 + strbuf_addstr(&author_email, x);
589 + else if (skip_prefix(sb.buf, "Date: ", &x))
590 + strbuf_addstr(&author_date, x);
591 + }
592 + fclose(fp);
593 +
594 + /* Skip pine's internal folder data */
595 + if (!strcmp(author_name.buf, "Mail System Internal Data")) {
596 + ret = 1;
597 + goto finish;
598 + }
599 +
600 + if (is_empty_file(am_path(state, "patch"))) {
601 + printf_ln(_("Patch is empty. Was it split wrong?"));
602 + exit(128);
603 + }
604 +
605 + strbuf_addstr(&msg, "\n\n");
606 + if (strbuf_read_file(&msg, am_path(state, "msg"), 0) < 0)
607 + die_errno(_("could not read '%s'"), am_path(state, "msg"));
608 + stripspace(&msg, 0);
609 +
610 + assert(!state->author_name);
611 + state->author_name = strbuf_detach(&author_name, NULL);
612 +
613 + assert(!state->author_email);
614 + state->author_email = strbuf_detach(&author_email, NULL);
615 +
616 + assert(!state->author_date);
617 + state->author_date = strbuf_detach(&author_date, NULL);
618 +
619 + assert(!state->msg);
620 + state->msg = strbuf_detach(&msg, &state->msg_len);
621 +
622 +finish:
623 + strbuf_release(&msg);
624 + strbuf_release(&author_date);
625 + strbuf_release(&author_email);
626 + strbuf_release(&author_name);
627 + strbuf_release(&sb);
628 + return ret;
629 +}
630 +
631 /**
632 * Applies all queued mail.
633 */
634 static void am_run(struct am_state *state)
635 {
636 while (state->cur <= state->last) {
637 + const char *mail = am_path(state, msgnum(state));
638 +
639 + if (!file_exists(mail))
640 + goto next;
641 +
642 + if (parse_mail(state, mail))
643 + goto next; /* mail should be skipped */
644 +
645 + write_author_script(state);
646 + write_commit_msg(state);
647
648 /* NEEDSWORK: Patch application not implemented yet */
649
650 +next:
651 am_next(state);
652 }
653