#include "merge-recursive.h"
#include "revision.h"
#include "log-tree.h"
+#include "notes-utils.h"
/**
* Returns 1 if the file is empty or does not exist, 0 otherwise.
KEEP_NON_PATCH /* pass -b flag to git-mailinfo */
};
+enum scissors_type {
+ SCISSORS_UNSET = -1,
+ SCISSORS_FALSE = 0, /* pass --no-scissors to git-mailinfo */
+ SCISSORS_TRUE /* pass --scissors to git-mailinfo */
+};
+
struct am_state {
/* state directory path */
char *dir;
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;
int utf8;
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;
+ const char *sign_commit;
int rebasing;
};
*/
static void am_state_init(struct am_state *state, const char *dir)
{
+ int gpgsign;
+
memset(state, 0, sizeof(*state));
assert(dir);
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;
}
/**
free(state->author_email);
free(state->author_date);
free(state->msg);
+ argv_array_clear(&state->git_apply_opts);
}
/**
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");
read_state_file(&sb, state, "messageid", 1);
state->message_id = !strcmp(sb.buf, "t");
+ read_state_file(&sb, state, "scissors", 1);
+ if (!strcmp(sb.buf, "t"))
+ state->scissors = SCISSORS_TRUE;
+ else if (!strcmp(sb.buf, "f"))
+ state->scissors = SCISSORS_FALSE;
+ 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);
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
* Splits out individual email patches from `paths`, where each path is either
* a mbox file or a Maildir. Returns 0 on success, -1 on failure.
*/
-static int split_mail_mbox(struct am_state *state, const char **paths)
+static int split_mail_mbox(struct am_state *state, const char **paths, int keep_cr)
{
struct child_process cp = CHILD_PROCESS_INIT;
struct strbuf last = STRBUF_INIT;
argv_array_pushf(&cp.args, "-d%d", state->prec);
argv_array_pushf(&cp.args, "-o%s", state->dir);
argv_array_push(&cp.args, "-b");
+ if (keep_cr)
+ argv_array_push(&cp.args, "--keep-cr");
argv_array_push(&cp.args, "--");
argv_array_pushv(&cp.args, paths);
* state->cur will be set to the index of the first mail, and state->last will
* be set to the index of the last mail.
*
+ * Set keep_cr to 0 to convert all lines ending with \r\n to end with \n, 1
+ * to disable this behavior, -1 to use the default configured setting.
+ *
* Returns 0 on success, -1 on failure.
*/
static int split_mail(struct am_state *state, enum patch_format patch_format,
- const char **paths)
+ const char **paths, int keep_cr)
{
+ if (keep_cr < 0) {
+ keep_cr = 0;
+ git_config_get_bool("am.keepcr", &keep_cr);
+ }
+
switch (patch_format) {
case PATCH_FORMAT_MBOX:
- return split_mail_mbox(state, paths);
+ return split_mail_mbox(state, paths, keep_cr);
default:
die("BUG: invalid patch_format");
}
* Setup a new am session for applying patches
*/
static void am_setup(struct am_state *state, enum patch_format patch_format,
- const char **paths)
+ const char **paths, int keep_cr)
{
unsigned char curr_head[GIT_SHA1_RAWSZ];
const char *str;
+ struct strbuf sb = STRBUF_INIT;
if (!patch_format)
patch_format = detect_patch_format(paths);
if (mkdir(state->dir, 0777) < 0 && errno != EEXIST)
die_errno(_("failed to create directory '%s'"), state->dir);
- if (split_mail(state, patch_format, paths) < 0) {
+ if (split_mail(state, patch_format, paths, keep_cr) < 0) {
am_destroy(state);
die(_("Failed to split patches."));
}
write_file(am_path(state, "messageid"), 1, state->message_id ? "t" : "f");
+ switch (state->scissors) {
+ case SCISSORS_UNSET:
+ str = "";
+ break;
+ case SCISSORS_FALSE:
+ str = "f";
+ break;
+ case SCISSORS_TRUE:
+ str = "t";
+ break;
+ default:
+ die("BUG: invalid value for state->scissors");
+ }
+
+ 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
write_file(am_path(state, "next"), 1, "%d", state->cur);
write_file(am_path(state, "last"), 1, "%d", state->last);
+
+ strbuf_release(&sb);
}
/**
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
if (state->message_id)
argv_array_push(&cp.args, "-m");
+ switch (state->scissors) {
+ case SCISSORS_UNSET:
+ break;
+ case SCISSORS_FALSE:
+ argv_array_push(&cp.args, "--no-scissors");
+ break;
+ case SCISSORS_TRUE:
+ argv_array_push(&cp.args, "--scissors");
+ break;
+ default:
+ die("BUG: invalid value for state->scissors");
+ }
+
argv_array_push(&cp.args, am_path(state, "msg"));
argv_array_push(&cp.args, am_path(state, "patch"));
* 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)
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;
}
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
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"));
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"));
}
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");
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);
+ }
+
strbuf_release(&sb);
}
write_commit_msg(state);
}
+ if (run_applypatch_msg_hook(state))
+ exit(1);
+
say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
apply_status = run_apply(state, NULL);
am_next(state);
}
+ if (!is_empty_file(am_path(state, "rewritten"))) {
+ assert(state->rebasing);
+ copy_notes_for_rebase(state);
+ run_post_rewrite_hook(state);
+ }
+
/*
* In rebasing mode, it's up to the caller to take care of
* housekeeping.
int cmd_am(int argc, const char **argv, const char *prefix)
{
struct am_state state;
+ int keep_cr = -1;
int patch_format = PATCH_FORMAT_UNKNOWN;
enum resume_mode resume = RESUME_FALSE;
N_("pass -b flag to git-mailinfo"), KEEP_NON_PATCH),
OPT_BOOL('m', "message-id", &state.message_id,
N_("pass -m flag to git-mailinfo")),
+ { OPTION_SET_INT, 0, "keep-cr", &keep_cr, NULL,
+ N_("pass --keep-cr flag to git-mailsplit for mbox format"),
+ PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1},
+ { OPTION_SET_INT, 0, "no-keep-cr", &keep_cr, NULL,
+ N_("do not pass --keep-cr flag to git-mailsplit independent of am.keepcr"),
+ 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,
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")),
+ { 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()
argv_array_push(&paths, mkpath("%s/%s", prefix, argv[i]));
}
- am_setup(&state, patch_format, paths.argv);
+ am_setup(&state, patch_format, paths.argv, keep_cr);
argv_array_clear(&paths);
}