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