]> git.ipfire.org Git - thirdparty/git.git/blobdiff - builtin/am.c
am: let command-line options override saved options
[thirdparty/git.git] / builtin / am.c
index 727cfb8f96d8aff02130747f1075a7c54a892e29..f81b74dd0b91cbcf84d2df87414805ffbaa7e06e 100644 (file)
@@ -23,6 +23,9 @@
 #include "merge-recursive.h"
 #include "revision.h"
 #include "log-tree.h"
+#include "notes-utils.h"
+#include "rerere.h"
+#include "prompt.h"
 
 /**
  * Returns 1 if the file is empty or does not exist, 0 otherwise.
@@ -63,9 +66,24 @@ static int linelen(const char *msg)
        return strchrnul(msg, '\n') - msg;
 }
 
+/**
+ * Returns true if `str` consists of only whitespace, false otherwise.
+ */
+static int str_isspace(const char *str)
+{
+       for (; *str; str++)
+               if (!isspace(*str))
+                       return 0;
+
+       return 1;
+}
+
 enum patch_format {
        PATCH_FORMAT_UNKNOWN = 0,
-       PATCH_FORMAT_MBOX
+       PATCH_FORMAT_MBOX,
+       PATCH_FORMAT_STGIT,
+       PATCH_FORMAT_STGIT_SERIES,
+       PATCH_FORMAT_HG
 };
 
 enum keep_type {
@@ -95,10 +113,14 @@ struct am_state {
        char *msg;
        size_t msg_len;
 
+       /* when --rebasing, records the original commit the patch came from */
+       unsigned char orig_commit[GIT_SHA1_RAWSZ];
+
        /* number of digits in patch filename */
        int prec;
 
        /* various operating modes and command line options */
+       int interactive;
        int threeway;
        int quiet;
        int signoff;
@@ -106,7 +128,12 @@ struct am_state {
        int keep; /* enum keep_type */
        int message_id;
        int scissors; /* enum scissors_type */
+       struct argv_array git_apply_opts;
        const char *resolvemsg;
+       int committer_date_is_author_date;
+       int ignore_date;
+       int allow_rerere_autoupdate;
+       const char *sign_commit;
        int rebasing;
 };
 
@@ -116,6 +143,8 @@ struct am_state {
  */
 static void am_state_init(struct am_state *state, const char *dir)
 {
+       int gpgsign;
+
        memset(state, 0, sizeof(*state));
 
        assert(dir);
@@ -123,11 +152,18 @@ static void am_state_init(struct am_state *state, const char *dir)
 
        state->prec = 4;
 
+       git_config_get_bool("am.threeway", &state->threeway);
+
        state->utf8 = 1;
 
        git_config_get_bool("am.messageid", &state->message_id);
 
        state->scissors = SCISSORS_UNSET;
+
+       argv_array_init(&state->git_apply_opts);
+
+       if (!git_config_get_bool("commit.gpgsign", &gpgsign))
+               state->sign_commit = gpgsign ? "" : NULL;
 }
 
 /**
@@ -140,6 +176,7 @@ static void am_state_release(struct am_state *state)
        free(state->author_email);
        free(state->author_date);
        free(state->msg);
+       argv_array_clear(&state->git_apply_opts);
 }
 
 /**
@@ -380,6 +417,11 @@ static void am_load(struct am_state *state)
 
        read_commit_msg(state);
 
+       if (read_state_file(&sb, state, "original-commit", 1) < 0)
+               hashclr(state->orig_commit);
+       else if (get_sha1_hex(sb.buf, state->orig_commit) < 0)
+               die(_("could not parse %s"), am_path(state, "original-commit"));
+
        read_state_file(&sb, state, "threeway", 1);
        state->threeway = !strcmp(sb.buf, "t");
 
@@ -411,6 +453,11 @@ static void am_load(struct am_state *state)
        else
                state->scissors = SCISSORS_UNSET;
 
+       read_state_file(&sb, state, "apply-opt", 1);
+       argv_array_clear(&state->git_apply_opts);
+       if (sq_dequote_to_argv_array(sb.buf, &state->git_apply_opts) < 0)
+               die(_("could not parse %s"), am_path(state, "apply-opt"));
+
        state->rebasing = !!file_exists(am_path(state, "rebasing"));
 
        strbuf_release(&sb);
@@ -429,6 +476,109 @@ static void am_destroy(const struct am_state *state)
        strbuf_release(&sb);
 }
 
+/**
+ * Runs applypatch-msg hook. Returns its exit code.
+ */
+static int run_applypatch_msg_hook(struct am_state *state)
+{
+       int ret;
+
+       assert(state->msg);
+       ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+
+       if (!ret) {
+               free(state->msg);
+               state->msg = NULL;
+               if (read_commit_msg(state) < 0)
+                       die(_("'%s' was deleted by the applypatch-msg hook"),
+                               am_path(state, "final-commit"));
+       }
+
+       return ret;
+}
+
+/**
+ * Runs post-rewrite hook. Returns it exit code.
+ */
+static int run_post_rewrite_hook(const struct am_state *state)
+{
+       struct child_process cp = CHILD_PROCESS_INIT;
+       const char *hook = find_hook("post-rewrite");
+       int ret;
+
+       if (!hook)
+               return 0;
+
+       argv_array_push(&cp.args, hook);
+       argv_array_push(&cp.args, "rebase");
+
+       cp.in = xopen(am_path(state, "rewritten"), O_RDONLY);
+       cp.stdout_to_stderr = 1;
+
+       ret = run_command(&cp);
+
+       close(cp.in);
+       return ret;
+}
+
+/**
+ * Reads the state directory's "rewritten" file, and copies notes from the old
+ * commits listed in the file to their rewritten commits.
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+static int copy_notes_for_rebase(const struct am_state *state)
+{
+       struct notes_rewrite_cfg *c;
+       struct strbuf sb = STRBUF_INIT;
+       const char *invalid_line = _("Malformed input line: '%s'.");
+       const char *msg = "Notes added by 'git rebase'";
+       FILE *fp;
+       int ret = 0;
+
+       assert(state->rebasing);
+
+       c = init_copy_notes_for_rewrite("rebase");
+       if (!c)
+               return 0;
+
+       fp = xfopen(am_path(state, "rewritten"), "r");
+
+       while (!strbuf_getline(&sb, fp, '\n')) {
+               unsigned char from_obj[GIT_SHA1_RAWSZ], to_obj[GIT_SHA1_RAWSZ];
+
+               if (sb.len != GIT_SHA1_HEXSZ * 2 + 1) {
+                       ret = error(invalid_line, sb.buf);
+                       goto finish;
+               }
+
+               if (get_sha1_hex(sb.buf, from_obj)) {
+                       ret = error(invalid_line, sb.buf);
+                       goto finish;
+               }
+
+               if (sb.buf[GIT_SHA1_HEXSZ] != ' ') {
+                       ret = error(invalid_line, sb.buf);
+                       goto finish;
+               }
+
+               if (get_sha1_hex(sb.buf + GIT_SHA1_HEXSZ + 1, to_obj)) {
+                       ret = error(invalid_line, sb.buf);
+                       goto finish;
+               }
+
+               if (copy_note_for_rewrite(c, from_obj, to_obj))
+                       ret = error(_("Failed to copy notes from '%s' to '%s'"),
+                                       sha1_to_hex(from_obj), sha1_to_hex(to_obj));
+       }
+
+finish:
+       finish_copy_notes_for_rewrite(c, msg);
+       fclose(fp);
+       strbuf_release(&sb);
+       return ret;
+}
+
 /**
  * Determines if the file looks like a piece of RFC2822 mail by grabbing all
  * non-indented lines and checking if they look like they begin with valid
@@ -479,6 +629,8 @@ static int detect_patch_format(const char **paths)
 {
        enum patch_format ret = PATCH_FORMAT_UNKNOWN;
        struct strbuf l1 = STRBUF_INIT;
+       struct strbuf l2 = STRBUF_INIT;
+       struct strbuf l3 = STRBUF_INIT;
        FILE *fp;
 
        /*
@@ -504,6 +656,33 @@ static int detect_patch_format(const char **paths)
                goto done;
        }
 
+       if (starts_with(l1.buf, "# This series applies on GIT commit")) {
+               ret = PATCH_FORMAT_STGIT_SERIES;
+               goto done;
+       }
+
+       if (!strcmp(l1.buf, "# HG changeset patch")) {
+               ret = PATCH_FORMAT_HG;
+               goto done;
+       }
+
+       strbuf_reset(&l2);
+       strbuf_getline_crlf(&l2, fp);
+       strbuf_reset(&l3);
+       strbuf_getline_crlf(&l3, fp);
+
+       /*
+        * If the second line is empty and the third is a From, Author or Date
+        * entry, this is likely an StGit patch.
+        */
+       if (l1.len && !l2.len &&
+               (starts_with(l3.buf, "From:") ||
+                starts_with(l3.buf, "Author:") ||
+                starts_with(l3.buf, "Date:"))) {
+               ret = PATCH_FORMAT_STGIT;
+               goto done;
+       }
+
        if (l1.len && is_mail(fp)) {
                ret = PATCH_FORMAT_MBOX;
                goto done;
@@ -543,6 +722,209 @@ static int split_mail_mbox(struct am_state *state, const char **paths, int keep_
        return 0;
 }
 
+/**
+ * Callback signature for split_mail_conv(). The foreign patch should be
+ * read from `in`, and the converted patch (in RFC2822 mail format) should be
+ * written to `out`. Return 0 on success, or -1 on failure.
+ */
+typedef int (*mail_conv_fn)(FILE *out, FILE *in, int keep_cr);
+
+/**
+ * Calls `fn` for each file in `paths` to convert the foreign patch to the
+ * RFC2822 mail format suitable for parsing with git-mailinfo.
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+static int split_mail_conv(mail_conv_fn fn, struct am_state *state,
+                       const char **paths, int keep_cr)
+{
+       static const char *stdin_only[] = {"-", NULL};
+       int i;
+
+       if (!*paths)
+               paths = stdin_only;
+
+       for (i = 0; *paths; paths++, i++) {
+               FILE *in, *out;
+               const char *mail;
+               int ret;
+
+               if (!strcmp(*paths, "-"))
+                       in = stdin;
+               else
+                       in = fopen(*paths, "r");
+
+               if (!in)
+                       return error(_("could not open '%s' for reading: %s"),
+                                       *paths, strerror(errno));
+
+               mail = mkpath("%s/%0*d", state->dir, state->prec, i + 1);
+
+               out = fopen(mail, "w");
+               if (!out)
+                       return error(_("could not open '%s' for writing: %s"),
+                                       mail, strerror(errno));
+
+               ret = fn(out, in, keep_cr);
+
+               fclose(out);
+               fclose(in);
+
+               if (ret)
+                       return error(_("could not parse patch '%s'"), *paths);
+       }
+
+       state->cur = 1;
+       state->last = i;
+       return 0;
+}
+
+/**
+ * A split_mail_conv() callback that converts an StGit patch to an RFC2822
+ * message suitable for parsing with git-mailinfo.
+ */
+static int stgit_patch_to_mail(FILE *out, FILE *in, int keep_cr)
+{
+       struct strbuf sb = STRBUF_INIT;
+       int subject_printed = 0;
+
+       while (!strbuf_getline(&sb, in, '\n')) {
+               const char *str;
+
+               if (str_isspace(sb.buf))
+                       continue;
+               else if (skip_prefix(sb.buf, "Author:", &str))
+                       fprintf(out, "From:%s\n", str);
+               else if (starts_with(sb.buf, "From") || starts_with(sb.buf, "Date"))
+                       fprintf(out, "%s\n", sb.buf);
+               else if (!subject_printed) {
+                       fprintf(out, "Subject: %s\n", sb.buf);
+                       subject_printed = 1;
+               } else {
+                       fprintf(out, "\n%s\n", sb.buf);
+                       break;
+               }
+       }
+
+       strbuf_reset(&sb);
+       while (strbuf_fread(&sb, 8192, in) > 0) {
+               fwrite(sb.buf, 1, sb.len, out);
+               strbuf_reset(&sb);
+       }
+
+       strbuf_release(&sb);
+       return 0;
+}
+
+/**
+ * This function only supports a single StGit series file in `paths`.
+ *
+ * Given an StGit series file, converts the StGit patches in the series into
+ * RFC2822 messages suitable for parsing with git-mailinfo, and queues them in
+ * the state directory.
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+static int split_mail_stgit_series(struct am_state *state, const char **paths,
+                                       int keep_cr)
+{
+       const char *series_dir;
+       char *series_dir_buf;
+       FILE *fp;
+       struct argv_array patches = ARGV_ARRAY_INIT;
+       struct strbuf sb = STRBUF_INIT;
+       int ret;
+
+       if (!paths[0] || paths[1])
+               return error(_("Only one StGIT patch series can be applied at once"));
+
+       series_dir_buf = xstrdup(*paths);
+       series_dir = dirname(series_dir_buf);
+
+       fp = fopen(*paths, "r");
+       if (!fp)
+               return error(_("could not open '%s' for reading: %s"), *paths,
+                               strerror(errno));
+
+       while (!strbuf_getline(&sb, fp, '\n')) {
+               if (*sb.buf == '#')
+                       continue; /* skip comment lines */
+
+               argv_array_push(&patches, mkpath("%s/%s", series_dir, sb.buf));
+       }
+
+       fclose(fp);
+       strbuf_release(&sb);
+       free(series_dir_buf);
+
+       ret = split_mail_conv(stgit_patch_to_mail, state, patches.argv, keep_cr);
+
+       argv_array_clear(&patches);
+       return ret;
+}
+
+/**
+ * A split_patches_conv() callback that converts a mercurial patch to a RFC2822
+ * message suitable for parsing with git-mailinfo.
+ */
+static int hg_patch_to_mail(FILE *out, FILE *in, int keep_cr)
+{
+       struct strbuf sb = STRBUF_INIT;
+
+       while (!strbuf_getline(&sb, in, '\n')) {
+               const char *str;
+
+               if (skip_prefix(sb.buf, "# User ", &str))
+                       fprintf(out, "From: %s\n", str);
+               else if (skip_prefix(sb.buf, "# Date ", &str)) {
+                       unsigned long timestamp;
+                       long tz, tz2;
+                       char *end;
+
+                       errno = 0;
+                       timestamp = strtoul(str, &end, 10);
+                       if (errno)
+                               return error(_("invalid timestamp"));
+
+                       if (!skip_prefix(end, " ", &str))
+                               return error(_("invalid Date line"));
+
+                       errno = 0;
+                       tz = strtol(str, &end, 10);
+                       if (errno)
+                               return error(_("invalid timezone offset"));
+
+                       if (*end)
+                               return error(_("invalid Date line"));
+
+                       /*
+                        * mercurial's timezone is in seconds west of UTC,
+                        * however git's timezone is in hours + minutes east of
+                        * UTC. Convert it.
+                        */
+                       tz2 = labs(tz) / 3600 * 100 + labs(tz) % 3600 / 60;
+                       if (tz > 0)
+                               tz2 = -tz2;
+
+                       fprintf(out, "Date: %s\n", show_date(timestamp, tz2, DATE_MODE(RFC2822)));
+               } else if (starts_with(sb.buf, "# ")) {
+                       continue;
+               } else {
+                       fprintf(out, "\n%s\n", sb.buf);
+                       break;
+               }
+       }
+
+       strbuf_reset(&sb);
+       while (strbuf_fread(&sb, 8192, in) > 0) {
+               fwrite(sb.buf, 1, sb.len, out);
+               strbuf_reset(&sb);
+       }
+
+       strbuf_release(&sb);
+       return 0;
+}
+
 /**
  * Splits a list of files/directories into individual email patches. Each path
  * in `paths` must be a file/directory that is formatted according to
@@ -571,6 +953,12 @@ static int split_mail(struct am_state *state, enum patch_format patch_format,
        switch (patch_format) {
        case PATCH_FORMAT_MBOX:
                return split_mail_mbox(state, paths, keep_cr);
+       case PATCH_FORMAT_STGIT:
+               return split_mail_conv(stgit_patch_to_mail, state, paths, keep_cr);
+       case PATCH_FORMAT_STGIT_SERIES:
+               return split_mail_stgit_series(state, paths, keep_cr);
+       case PATCH_FORMAT_HG:
+               return split_mail_conv(hg_patch_to_mail, state, paths, keep_cr);
        default:
                die("BUG: invalid patch_format");
        }
@@ -585,6 +973,7 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,
 {
        unsigned char curr_head[GIT_SHA1_RAWSZ];
        const char *str;
+       struct strbuf sb = STRBUF_INIT;
 
        if (!patch_format)
                patch_format = detect_patch_format(paths);
@@ -647,6 +1036,9 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,
 
        write_file(am_path(state, "scissors"), 1, "%s", str);
 
+       sq_quote_argv(&sb, state->git_apply_opts.argv, 0);
+       write_file(am_path(state, "apply-opt"), 1, "%s", sb.buf);
+
        if (state->rebasing)
                write_file(am_path(state, "rebasing"), 1, "%s", "");
        else
@@ -671,6 +1063,8 @@ static void am_setup(struct am_state *state, enum patch_format patch_format,
        write_file(am_path(state, "next"), 1, "%d", state->cur);
 
        write_file(am_path(state, "last"), 1, "%d", state->last);
+
+       strbuf_release(&sb);
 }
 
 /**
@@ -697,6 +1091,9 @@ static void am_next(struct am_state *state)
        unlink(am_path(state, "author-script"));
        unlink(am_path(state, "final-commit"));
 
+       hashclr(state->orig_commit);
+       unlink(am_path(state, "original-commit"));
+
        if (!get_sha1("HEAD", head))
                write_file(am_path(state, "abort-safety"), 1, "%s", sha1_to_hex(head));
        else
@@ -778,7 +1175,7 @@ static void NORETURN die_user_resolve(const struct am_state *state)
        if (state->resolvemsg) {
                printf_ln("%s", state->resolvemsg);
        } else {
-               const char *cmdline = "git am";
+               const char *cmdline = state->interactive ? "git am -i" : "git am";
 
                printf_ln(_("When you have resolved this problem, run \"%s --continue\"."), cmdline);
                printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), cmdline);
@@ -1010,11 +1407,43 @@ static void write_commit_patch(const struct am_state *state, struct commit *comm
        log_tree_commit(&rev_info, commit);
 }
 
+/**
+ * Writes the diff of the index against HEAD as a patch to the state
+ * directory's "patch" file.
+ */
+static void write_index_patch(const struct am_state *state)
+{
+       struct tree *tree;
+       unsigned char head[GIT_SHA1_RAWSZ];
+       struct rev_info rev_info;
+       FILE *fp;
+
+       if (!get_sha1_tree("HEAD", head))
+               tree = lookup_tree(head);
+       else
+               tree = lookup_tree(EMPTY_TREE_SHA1_BIN);
+
+       fp = xfopen(am_path(state, "patch"), "w");
+       init_revisions(&rev_info, NULL);
+       rev_info.diff = 1;
+       rev_info.disable_stdin = 1;
+       rev_info.no_commit_id = 1;
+       rev_info.diffopt.output_format = DIFF_FORMAT_PATCH;
+       rev_info.diffopt.use_color = 0;
+       rev_info.diffopt.file = fp;
+       rev_info.diffopt.close_file = 1;
+       add_pending_object(&rev_info, &tree->object, "");
+       diff_setup_done(&rev_info.diffopt);
+       run_diff_index(&rev_info, 1);
+}
+
 /**
  * Like parse_mail(), but parses the mail by looking up its commit ID
  * directly. This is used in --rebasing mode to bypass git-mailinfo's munging
  * of patches.
  *
+ * state->orig_commit will be set to the original commit ID.
+ *
  * Will always return 0 as the patch should never be skipped.
  */
 static int parse_mail_rebase(struct am_state *state, const char *mail)
@@ -1031,6 +1460,10 @@ static int parse_mail_rebase(struct am_state *state, const char *mail)
 
        write_commit_patch(state, commit);
 
+       hashcpy(state->orig_commit, commit_sha1);
+       write_file(am_path(state, "original-commit"), 1, "%s",
+                       sha1_to_hex(commit_sha1));
+
        return 0;
 }
 
@@ -1058,6 +1491,8 @@ static int run_apply(const struct am_state *state, const char *index_file)
 
        argv_array_push(&cp.args, "apply");
 
+       argv_array_pushv(&cp.args, state->git_apply_opts.argv);
+
        if (index_file)
                argv_array_push(&cp.args, "--cached");
        else
@@ -1084,6 +1519,7 @@ static int build_fake_ancestor(const struct am_state *state, const char *index_f
 
        cp.git_cmd = 1;
        argv_array_push(&cp.args, "apply");
+       argv_array_pushv(&cp.args, state->git_apply_opts.argv);
        argv_array_pushf(&cp.args, "--build-fake-ancestor=%s", index_file);
        argv_array_push(&cp.args, am_path(state, "patch"));
 
@@ -1165,6 +1601,7 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa
                o.verbosity = 0;
 
        if (merge_recursive_generic(&o, our_tree, his_tree, 1, bases, &result)) {
+               rerere(state->allow_rerere_autoupdate);
                free(his_tree_name);
                return error(_("Failed to merge in the changes."));
        }
@@ -1187,6 +1624,9 @@ static void do_commit(const struct am_state *state)
        const char *reflog_msg, *author;
        struct strbuf sb = STRBUF_INIT;
 
+       if (run_hook_le(NULL, "pre-applypatch", NULL))
+               exit(1);
+
        if (write_cache_as_tree(tree, 0, NULL))
                die(_("git write-tree failed to write a tree"));
 
@@ -1199,10 +1639,15 @@ static void do_commit(const struct am_state *state)
        }
 
        author = fmt_ident(state->author_name, state->author_email,
-                       state->author_date, IDENT_STRICT);
+                       state->ignore_date ? NULL : state->author_date,
+                       IDENT_STRICT);
+
+       if (state->committer_date_is_author_date)
+               setenv("GIT_COMMITTER_DATE",
+                       state->ignore_date ? "" : state->author_date, 1);
 
        if (commit_tree(state->msg, state->msg_len, tree, parents, commit,
-                               author, NULL))
+                               author, state->sign_commit))
                die(_("failed to write commit object"));
 
        reflog_msg = getenv("GIT_REFLOG_ACTION");
@@ -1214,6 +1659,17 @@ static void do_commit(const struct am_state *state)
 
        update_ref(sb.buf, "HEAD", commit, ptr, 0, UPDATE_REFS_DIE_ON_ERR);
 
+       if (state->rebasing) {
+               FILE *fp = xfopen(am_path(state, "rewritten"), "a");
+
+               assert(!is_null_sha1(state->orig_commit));
+               fprintf(fp, "%s ", sha1_to_hex(state->orig_commit));
+               fprintf(fp, "%s\n", sha1_to_hex(commit));
+               fclose(fp);
+       }
+
+       run_hook_le(NULL, "post-applypatch", NULL);
+
        strbuf_release(&sb);
 }
 
@@ -1232,6 +1688,65 @@ static void validate_resume_state(const struct am_state *state)
                        am_path(state, "author-script"));
 }
 
+/**
+ * Interactively prompt the user on whether the current patch should be
+ * applied.
+ *
+ * Returns 0 if the user chooses to apply the patch, 1 if the user chooses to
+ * skip it.
+ */
+static int do_interactive(struct am_state *state)
+{
+       assert(state->msg);
+
+       if (!isatty(0))
+               die(_("cannot be interactive without stdin connected to a terminal."));
+
+       for (;;) {
+               const char *reply;
+
+               puts(_("Commit Body is:"));
+               puts("--------------------------");
+               printf("%s", state->msg);
+               puts("--------------------------");
+
+               /*
+                * TRANSLATORS: Make sure to include [y], [n], [e], [v] and [a]
+                * in your translation. The program will only accept English
+                * input at this point.
+                */
+               reply = git_prompt(_("Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all: "), PROMPT_ECHO);
+
+               if (!reply) {
+                       continue;
+               } else if (*reply == 'y' || *reply == 'Y') {
+                       return 0;
+               } else if (*reply == 'a' || *reply == 'A') {
+                       state->interactive = 0;
+                       return 0;
+               } else if (*reply == 'n' || *reply == 'N') {
+                       return 1;
+               } else if (*reply == 'e' || *reply == 'E') {
+                       struct strbuf msg = STRBUF_INIT;
+
+                       if (!launch_editor(am_path(state, "final-commit"), &msg, NULL)) {
+                               free(state->msg);
+                               state->msg = strbuf_detach(&msg, &state->msg_len);
+                       }
+                       strbuf_release(&msg);
+               } else if (*reply == 'v' || *reply == 'V') {
+                       const char *pager = git_pager(1);
+                       struct child_process cp = CHILD_PROCESS_INIT;
+
+                       if (!pager)
+                               pager = "cat";
+                       argv_array_push(&cp.args, pager);
+                       argv_array_push(&cp.args, am_path(state, "patch"));
+                       run_command(&cp);
+               }
+       }
+}
+
 /**
  * Applies all queued mail.
  *
@@ -1264,7 +1779,6 @@ static void am_run(struct am_state *state, int resume)
 
                if (resume) {
                        validate_resume_state(state);
-                       resume = 0;
                } else {
                        int skip;
 
@@ -1280,6 +1794,12 @@ static void am_run(struct am_state *state, int resume)
                        write_commit_msg(state);
                }
 
+               if (state->interactive && do_interactive(state))
+                       goto next;
+
+               if (run_applypatch_msg_hook(state))
+                       exit(1);
+
                say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
 
                apply_status = run_apply(state, NULL);
@@ -1320,6 +1840,16 @@ static void am_run(struct am_state *state, int resume)
 
 next:
                am_next(state);
+
+               if (resume)
+                       am_load(state);
+               resume = 0;
+       }
+
+       if (!is_empty_file(am_path(state, "rewritten"))) {
+               assert(state->rebasing);
+               copy_notes_for_rebase(state);
+               run_post_rewrite_hook(state);
        }
 
        /*
@@ -1356,9 +1886,19 @@ static void am_resolve(struct am_state *state)
                die_user_resolve(state);
        }
 
+       if (state->interactive) {
+               write_index_patch(state);
+               if (do_interactive(state))
+                       goto next;
+       }
+
+       rerere(0);
+
        do_commit(state);
 
+next:
        am_next(state);
+       am_load(state);
        am_run(state, 0);
 }
 
@@ -1455,6 +1995,21 @@ static int clean_index(const unsigned char *head, const unsigned char *remote)
        return 0;
 }
 
+/**
+ * Resets rerere's merge resolution metadata.
+ */
+static void am_rerere_clear(void)
+{
+       struct string_list merge_rr = STRING_LIST_INIT_DUP;
+       int fd = setup_rerere(&merge_rr, 0);
+
+       if (fd < 0)
+               return;
+
+       rerere_clear(&merge_rr);
+       string_list_clear(&merge_rr, 1);
+}
+
 /**
  * Resume the current am session by skipping the current patch.
  */
@@ -1462,6 +2017,8 @@ static void am_skip(struct am_state *state)
 {
        unsigned char head[GIT_SHA1_RAWSZ];
 
+       am_rerere_clear();
+
        if (get_sha1("HEAD", head))
                hashcpy(head, EMPTY_TREE_SHA1_BIN);
 
@@ -1469,6 +2026,7 @@ static void am_skip(struct am_state *state)
                die(_("failed to clean index"));
 
        am_next(state);
+       am_load(state);
        am_run(state, 0);
 }
 
@@ -1519,6 +2077,8 @@ static void am_abort(struct am_state *state)
                return;
        }
 
+       am_rerere_clear();
+
        curr_branch = resolve_refdup("HEAD", 0, curr_head, NULL);
        has_curr_head = !is_null_sha1(curr_head);
        if (!has_curr_head)
@@ -1551,6 +2111,12 @@ static int parse_opt_patchformat(const struct option *opt, const char *arg, int
 
        if (!strcmp(arg, "mbox"))
                *opt_value = PATCH_FORMAT_MBOX;
+       else if (!strcmp(arg, "stgit"))
+               *opt_value = PATCH_FORMAT_STGIT;
+       else if (!strcmp(arg, "stgit-series"))
+               *opt_value = PATCH_FORMAT_STGIT_SERIES;
+       else if (!strcmp(arg, "hg"))
+               *opt_value = PATCH_FORMAT_HG;
        else
                return error(_("Invalid value for --patch-format: %s"), arg);
        return 0;
@@ -1567,9 +2133,11 @@ enum resume_mode {
 int cmd_am(int argc, const char **argv, const char *prefix)
 {
        struct am_state state;
+       int binary = -1;
        int keep_cr = -1;
        int patch_format = PATCH_FORMAT_UNKNOWN;
        enum resume_mode resume = RESUME_FALSE;
+       int in_progress;
 
        const char * const usage[] = {
                N_("git am [options] [(<mbox>|<Maildir>)...]"),
@@ -1578,6 +2146,10 @@ int cmd_am(int argc, const char **argv, const char *prefix)
        };
 
        struct option options[] = {
+               OPT_BOOL('i', "interactive", &state.interactive,
+                       N_("run interactively")),
+               OPT_HIDDEN_BOOL('b', "binary", &binary,
+                       N_("(historical option -- no-op")),
                OPT_BOOL('3', "3way", &state.threeway,
                        N_("allow fall back on 3way merging if needed")),
                OPT__QUIET(&state.quiet, N_("be quiet")),
@@ -1599,9 +2171,36 @@ int cmd_am(int argc, const char **argv, const char *prefix)
                  PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 0},
                OPT_BOOL('c', "scissors", &state.scissors,
                        N_("strip everything before a scissors line")),
+               OPT_PASSTHRU_ARGV(0, "whitespace", &state.git_apply_opts, N_("action"),
+                       N_("pass it through git-apply"),
+                       0),
+               OPT_PASSTHRU_ARGV(0, "ignore-space-change", &state.git_apply_opts, NULL,
+                       N_("pass it through git-apply"),
+                       PARSE_OPT_NOARG),
+               OPT_PASSTHRU_ARGV(0, "ignore-whitespace", &state.git_apply_opts, NULL,
+                       N_("pass it through git-apply"),
+                       PARSE_OPT_NOARG),
+               OPT_PASSTHRU_ARGV(0, "directory", &state.git_apply_opts, N_("root"),
+                       N_("pass it through git-apply"),
+                       0),
+               OPT_PASSTHRU_ARGV(0, "exclude", &state.git_apply_opts, N_("path"),
+                       N_("pass it through git-apply"),
+                       0),
+               OPT_PASSTHRU_ARGV(0, "include", &state.git_apply_opts, N_("path"),
+                       N_("pass it through git-apply"),
+                       0),
+               OPT_PASSTHRU_ARGV('C', NULL, &state.git_apply_opts, N_("n"),
+                       N_("pass it through git-apply"),
+                       0),
+               OPT_PASSTHRU_ARGV('p', NULL, &state.git_apply_opts, N_("num"),
+                       N_("pass it through git-apply"),
+                       0),
                OPT_CALLBACK(0, "patch-format", &patch_format, N_("format"),
                        N_("format the patch(es) are in"),
                        parse_opt_patchformat),
+               OPT_PASSTHRU_ARGV(0, "reject", &state.git_apply_opts, NULL,
+                       N_("pass it through git-apply"),
+                       PARSE_OPT_NOARG),
                OPT_STRING(0, "resolvemsg", &state.resolvemsg, NULL,
                        N_("override error message when patch failure occurs")),
                OPT_CMDMODE(0, "continue", &resume,
@@ -1616,36 +2215,41 @@ int cmd_am(int argc, const char **argv, const char *prefix)
                OPT_CMDMODE(0, "abort", &resume,
                        N_("restore the original branch and abort the patching operation."),
                        RESUME_ABORT),
+               OPT_BOOL(0, "committer-date-is-author-date",
+                       &state.committer_date_is_author_date,
+                       N_("lie about committer date")),
+               OPT_BOOL(0, "ignore-date", &state.ignore_date,
+                       N_("use current timestamp for author date")),
+               OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate),
+               { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"),
+                 N_("GPG-sign commits"),
+                 PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
                OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing,
                        N_("(internal use for git-rebase)")),
                OPT_END()
        };
 
-       /*
-        * NEEDSWORK: Once all the features of git-am.sh have been
-        * re-implemented in builtin/am.c, this preamble can be removed.
-        */
-       if (!getenv("_GIT_USE_BUILTIN_AM")) {
-               const char *path = mkpath("%s/git-am", git_exec_path());
-
-               if (sane_execvp(path, (char **)argv) < 0)
-                       die_errno("could not exec %s", path);
-       } else {
-               prefix = setup_git_directory();
-               trace_repo_setup(prefix);
-               setup_work_tree();
-       }
-
        git_config(git_default_config, NULL);
 
        am_state_init(&state, git_path("rebase-apply"));
 
+       in_progress = am_in_progress(&state);
+       if (in_progress)
+               am_load(&state);
+
        argc = parse_options(argc, argv, prefix, options, usage, 0);
 
+       if (binary >= 0)
+               fprintf_ln(stderr, _("The -b/--binary option has been a no-op for long time, and\n"
+                               "it will be removed. Please do not use it anymore."));
+
+       /* Ensure a valid committer ident can be constructed */
+       git_committer_info(IDENT_STRICT);
+
        if (read_index_preload(&the_index, NULL) < 0)
                die(_("failed to read the index"));
 
-       if (am_in_progress(&state)) {
+       if (in_progress) {
                /*
                 * Catch user error to feed us patches when there is a session
                 * in progress:
@@ -1663,8 +2267,6 @@ int cmd_am(int argc, const char **argv, const char *prefix)
 
                if (resume == RESUME_FALSE)
                        resume = RESUME_APPLY;
-
-               am_load(&state);
        } else {
                struct argv_array paths = ARGV_ARRAY_INIT;
                int i;