builtin / am.con commit builtin-am: split out mbox/maildir patches with git-mailsplit (11c2177)
   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}