1/*
2 * Builtin "git am"
3 *
4 * Based on git-am.sh by Junio C Hamano.
5 */
6#include "cache.h"
7#include "builtin.h"
8#include "exec_cmd.h"
9#include "parse-options.h"
10#include "dir.h"
11#include "run-command.h"
12
13enum patch_format {
14 PATCH_FORMAT_UNKNOWN = 0,
15 PATCH_FORMAT_MBOX
16};
17
18struct am_state {
19 /* state directory path */
20 char *dir;
21
22 /* current and last patch numbers, 1-indexed */
23 int cur;
24 int last;
25
26 /* number of digits in patch filename */
27 int prec;
28};
29
30/**
31 * Initializes am_state with the default values. The state directory is set to
32 * dir.
33 */
34static void am_state_init(struct am_state *state, const char *dir)
35{
36 memset(state, 0, sizeof(*state));
37
38 assert(dir);
39 state->dir = xstrdup(dir);
40
41 state->prec = 4;
42}
43
44/**
45 * Releases memory allocated by an am_state.
46 */
47static void am_state_release(struct am_state *state)
48{
49 free(state->dir);
50}
51
52/**
53 * Returns path relative to the am_state directory.
54 */
55static inline const char *am_path(const struct am_state *state, const char *path)
56{
57 return mkpath("%s/%s", state->dir, path);
58}
59
60/**
61 * Returns 1 if there is an am session in progress, 0 otherwise.
62 */
63static int am_in_progress(const struct am_state *state)
64{
65 struct stat st;
66
67 if (lstat(state->dir, &st) < 0 || !S_ISDIR(st.st_mode))
68 return 0;
69 if (lstat(am_path(state, "last"), &st) || !S_ISREG(st.st_mode))
70 return 0;
71 if (lstat(am_path(state, "next"), &st) || !S_ISREG(st.st_mode))
72 return 0;
73 return 1;
74}
75
76/**
77 * Reads the contents of `file` in the `state` directory into `sb`. Returns the
78 * number of bytes read on success, -1 if the file does not exist. If `trim` is
79 * set, trailing whitespace will be removed.
80 */
81static int read_state_file(struct strbuf *sb, const struct am_state *state,
82 const char *file, int trim)
83{
84 strbuf_reset(sb);
85
86 if (strbuf_read_file(sb, am_path(state, file), 0) >= 0) {
87 if (trim)
88 strbuf_trim(sb);
89
90 return sb->len;
91 }
92
93 if (errno == ENOENT)
94 return -1;
95
96 die_errno(_("could not read '%s'"), am_path(state, file));
97}
98
99/**
100 * Loads state from disk.
101 */
102static void am_load(struct am_state *state)
103{
104 struct strbuf sb = STRBUF_INIT;
105
106 if (read_state_file(&sb, state, "next", 1) < 0)
107 die("BUG: state file 'next' does not exist");
108 state->cur = strtol(sb.buf, NULL, 10);
109
110 if (read_state_file(&sb, state, "last", 1) < 0)
111 die("BUG: state file 'last' does not exist");
112 state->last = strtol(sb.buf, NULL, 10);
113
114 strbuf_release(&sb);
115}
116
117/**
118 * Removes the am_state directory, forcefully terminating the current am
119 * session.
120 */
121static void am_destroy(const struct am_state *state)
122{
123 struct strbuf sb = STRBUF_INIT;
124
125 strbuf_addstr(&sb, state->dir);
126 remove_dir_recursively(&sb, 0);
127 strbuf_release(&sb);
128}
129
130/**
131 * Splits out individual email patches from `paths`, where each path is either
132 * a mbox file or a Maildir. Returns 0 on success, -1 on failure.
133 */
134static int split_mail_mbox(struct am_state *state, const char **paths)
135{
136 struct child_process cp = CHILD_PROCESS_INIT;
137 struct strbuf last = STRBUF_INIT;
138
139 cp.git_cmd = 1;
140 argv_array_push(&cp.args, "mailsplit");
141 argv_array_pushf(&cp.args, "-d%d", state->prec);
142 argv_array_pushf(&cp.args, "-o%s", state->dir);
143 argv_array_push(&cp.args, "-b");
144 argv_array_push(&cp.args, "--");
145 argv_array_pushv(&cp.args, paths);
146
147 if (capture_command(&cp, &last, 8))
148 return -1;
149
150 state->cur = 1;
151 state->last = strtol(last.buf, NULL, 10);
152
153 return 0;
154}
155
156/**
157 * Splits a list of files/directories into individual email patches. Each path
158 * in `paths` must be a file/directory that is formatted according to
159 * `patch_format`.
160 *
161 * Once split out, the individual email patches will be stored in the state
162 * directory, with each patch's filename being its index, padded to state->prec
163 * digits.
164 *
165 * state->cur will be set to the index of the first mail, and state->last will
166 * be set to the index of the last mail.
167 *
168 * Returns 0 on success, -1 on failure.
169 */
170static int split_mail(struct am_state *state, enum patch_format patch_format,
171 const char **paths)
172{
173 switch (patch_format) {
174 case PATCH_FORMAT_MBOX:
175 return split_mail_mbox(state, paths);
176 default:
177 die("BUG: invalid patch_format");
178 }
179 return -1;
180}
181
182/**
183 * Setup a new am session for applying patches
184 */
185static void am_setup(struct am_state *state, enum patch_format patch_format,
186 const char **paths)
187{
188 if (mkdir(state->dir, 0777) < 0 && errno != EEXIST)
189 die_errno(_("failed to create directory '%s'"), state->dir);
190
191 if (split_mail(state, patch_format, paths) < 0) {
192 am_destroy(state);
193 die(_("Failed to split patches."));
194 }
195
196 /*
197 * NOTE: Since the "next" and "last" files determine if an am_state
198 * session is in progress, they should be written last.
199 */
200
201 write_file(am_path(state, "next"), 1, "%d", state->cur);
202
203 write_file(am_path(state, "last"), 1, "%d", state->last);
204}
205
206/**
207 * Increments the patch pointer, and cleans am_state for the application of the
208 * next patch.
209 */
210static void am_next(struct am_state *state)
211{
212 state->cur++;
213 write_file(am_path(state, "next"), 1, "%d", state->cur);
214}
215
216/**
217 * Applies all queued mail.
218 */
219static void am_run(struct am_state *state)
220{
221 while (state->cur <= state->last) {
222
223 /* NEEDSWORK: Patch application not implemented yet */
224
225 am_next(state);
226 }
227
228 am_destroy(state);
229}
230
231/**
232 * parse_options() callback that validates and sets opt->value to the
233 * PATCH_FORMAT_* enum value corresponding to `arg`.
234 */
235static int parse_opt_patchformat(const struct option *opt, const char *arg, int unset)
236{
237 int *opt_value = opt->value;
238
239 if (!strcmp(arg, "mbox"))
240 *opt_value = PATCH_FORMAT_MBOX;
241 else
242 return error(_("Invalid value for --patch-format: %s"), arg);
243 return 0;
244}
245
246int cmd_am(int argc, const char **argv, const char *prefix)
247{
248 struct am_state state;
249 int patch_format = PATCH_FORMAT_UNKNOWN;
250
251 const char * const usage[] = {
252 N_("git am [options] [(<mbox>|<Maildir>)...]"),
253 NULL
254 };
255
256 struct option options[] = {
257 OPT_CALLBACK(0, "patch-format", &patch_format, N_("format"),
258 N_("format the patch(es) are in"),
259 parse_opt_patchformat),
260 OPT_END()
261 };
262
263 /*
264 * NEEDSWORK: Once all the features of git-am.sh have been
265 * re-implemented in builtin/am.c, this preamble can be removed.
266 */
267 if (!getenv("_GIT_USE_BUILTIN_AM")) {
268 const char *path = mkpath("%s/git-am", git_exec_path());
269
270 if (sane_execvp(path, (char **)argv) < 0)
271 die_errno("could not exec %s", path);
272 } else {
273 prefix = setup_git_directory();
274 trace_repo_setup(prefix);
275 setup_work_tree();
276 }
277
278 git_config(git_default_config, NULL);
279
280 am_state_init(&state, git_path("rebase-apply"));
281
282 argc = parse_options(argc, argv, prefix, options, usage, 0);
283
284 if (am_in_progress(&state))
285 am_load(&state);
286 else {
287 struct argv_array paths = ARGV_ARRAY_INIT;
288 int i;
289
290 for (i = 0; i < argc; i++) {
291 if (is_absolute_path(argv[i]) || !prefix)
292 argv_array_push(&paths, argv[i]);
293 else
294 argv_array_push(&paths, mkpath("%s/%s", prefix, argv[i]));
295 }
296
297 am_setup(&state, patch_format, paths.argv);
298
299 argv_array_clear(&paths);
300 }
301
302 am_run(&state);
303
304 am_state_release(&state);
305
306 return 0;
307}