Merge branch 'pw/sequencer-in-process-commit'

The sequencer infrastructure is shared across "git cherry-pick",
"git rebase -i", etc., and has always spawned "git commit" when it
needs to create a commit.  It has been taught to do so internally,
when able, by reusing the codepath "git commit" itself uses, which
gives performance boost for a few tens of percents in some sample
scenarios.

* pw/sequencer-in-process-commit:
  sequencer: run 'prepare-commit-msg' hook
  t7505: add tests for cherry-pick and rebase -i/-p
  t7505: style fixes
  sequencer: assign only free()able strings to gpg_sign
  sequencer: improve config handling
  t3512/t3513: remove KNOWN_FAILURE_CHERRY_PICK_SEES_EMPTY_COMMIT=1
  sequencer: try to commit without forking 'git commit'
  sequencer: load commit related config
  sequencer: simplify adding Signed-off-by: trailer
  commit: move print_commit_summary() to libgit
  commit: move post-rewrite code to libgit
  Add a function to update HEAD after creating a commit
  commit: move empty message checks to libgit
  t3404: check intermediate squash messages
This commit is contained in:
Junio C Hamano 2018-02-13 13:39:15 -08:00
commit 0f57f731ea
11 changed files with 765 additions and 280 deletions

View File

@ -31,9 +31,7 @@
#include "gpg-interface.h"
#include "column.h"
#include "sequencer.h"
#include "notes-utils.h"
#include "mailmap.h"
#include "sigchain.h"
static const char * const builtin_commit_usage[] = {
N_("git commit [<options>] [--] <pathspec>..."),
@ -45,31 +43,6 @@ static const char * const builtin_status_usage[] = {
NULL
};
static const char implicit_ident_advice_noconfig[] =
N_("Your name and email address were configured automatically based\n"
"on your username and hostname. Please check that they are accurate.\n"
"You can suppress this message by setting them explicitly. Run the\n"
"following command and follow the instructions in your editor to edit\n"
"your configuration file:\n"
"\n"
" git config --global --edit\n"
"\n"
"After doing this, you may fix the identity used for this commit with:\n"
"\n"
" git commit --amend --reset-author\n");
static const char implicit_ident_advice_config[] =
N_("Your name and email address were configured automatically based\n"
"on your username and hostname. Please check that they are accurate.\n"
"You can suppress this message by setting them explicitly:\n"
"\n"
" git config --global user.name \"Your Name\"\n"
" git config --global user.email you@example.com\n"
"\n"
"After doing this, you may fix the identity used for this commit with:\n"
"\n"
" git commit --amend --reset-author\n");
static const char empty_amend_advice[] =
N_("You asked to amend the most recent commit, but doing so would make\n"
"it empty. You can repeat your command with --allow-empty, or you can\n"
@ -93,8 +66,6 @@ N_("If you wish to skip this commit, use:\n"
"Then \"git cherry-pick --continue\" will resume cherry-picking\n"
"the remaining commits.\n");
static GIT_PATH_FUNC(git_path_commit_editmsg, "COMMIT_EDITMSG")
static const char *use_message_buffer;
static struct lock_file index_lock; /* real index */
static struct lock_file false_lock; /* used only for partial commits */
@ -128,12 +99,7 @@ static char *sign_commit;
* if editor is used, and only the whitespaces if the message
* is specified explicitly.
*/
static enum {
CLEANUP_SPACE,
CLEANUP_NONE,
CLEANUP_SCISSORS,
CLEANUP_ALL
} cleanup_mode;
static enum commit_msg_cleanup_mode cleanup_mode;
static const char *cleanup_arg;
static enum commit_whence whence;
@ -673,7 +639,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
struct strbuf sb = STRBUF_INIT;
const char *hook_arg1 = NULL;
const char *hook_arg2 = NULL;
int clean_message_contents = (cleanup_mode != CLEANUP_NONE);
int clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);
int old_display_comment_prefix;
/* This checks and barfs if author is badly specified */
@ -814,7 +780,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
struct ident_split ci, ai;
if (whence != FROM_COMMIT) {
if (cleanup_mode == CLEANUP_SCISSORS)
if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS)
wt_status_add_cut_line(s->fp);
status_printf_ln(s, GIT_COLOR_NORMAL,
whence == FROM_MERGE
@ -834,14 +800,15 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
}
fprintf(s->fp, "\n");
if (cleanup_mode == CLEANUP_ALL)
if (cleanup_mode == COMMIT_MSG_CLEANUP_ALL)
status_printf(s, GIT_COLOR_NORMAL,
_("Please enter the commit message for your changes."
" Lines starting\nwith '%c' will be ignored, and an empty"
" message aborts the commit.\n"), comment_line_char);
else if (cleanup_mode == CLEANUP_SCISSORS && whence == FROM_COMMIT)
else if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS &&
whence == FROM_COMMIT)
wt_status_add_cut_line(s->fp);
else /* CLEANUP_SPACE, that is. */
else /* COMMIT_MSG_CLEANUP_SPACE, that is. */
status_printf(s, GIT_COLOR_NORMAL,
_("Please enter the commit message for your changes."
" Lines starting\n"
@ -986,65 +953,6 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
return 1;
}
static int rest_is_empty(struct strbuf *sb, int start)
{
int i, eol;
const char *nl;
/* Check if the rest is just whitespace and Signed-off-by's. */
for (i = start; i < sb->len; i++) {
nl = memchr(sb->buf + i, '\n', sb->len - i);
if (nl)
eol = nl - sb->buf;
else
eol = sb->len;
if (strlen(sign_off_header) <= eol - i &&
starts_with(sb->buf + i, sign_off_header)) {
i = eol;
continue;
}
while (i < eol)
if (!isspace(sb->buf[i++]))
return 0;
}
return 1;
}
/*
* Find out if the message in the strbuf contains only whitespace and
* Signed-off-by lines.
*/
static int message_is_empty(struct strbuf *sb)
{
if (cleanup_mode == CLEANUP_NONE && sb->len)
return 0;
return rest_is_empty(sb, 0);
}
/*
* See if the user edited the message in the editor or left what
* was in the template intact
*/
static int template_untouched(struct strbuf *sb)
{
struct strbuf tmpl = STRBUF_INIT;
const char *start;
if (cleanup_mode == CLEANUP_NONE && sb->len)
return 0;
if (!template_file || strbuf_read_file(&tmpl, template_file, 0) <= 0)
return 0;
strbuf_stripspace(&tmpl, cleanup_mode == CLEANUP_ALL);
if (!skip_prefix(sb->buf, tmpl.buf, &start))
start = sb->buf;
strbuf_release(&tmpl);
return rest_is_empty(sb, start - sb->buf);
}
static const char *find_author_by_nickname(const char *name)
{
struct rev_info revs;
@ -1229,15 +1137,17 @@ static int parse_and_validate_options(int argc, const char *argv[],
if (argc == 0 && (also || (only && !amend && !allow_empty)))
die(_("No paths with --include/--only does not make sense."));
if (!cleanup_arg || !strcmp(cleanup_arg, "default"))
cleanup_mode = use_editor ? CLEANUP_ALL : CLEANUP_SPACE;
cleanup_mode = use_editor ? COMMIT_MSG_CLEANUP_ALL :
COMMIT_MSG_CLEANUP_SPACE;
else if (!strcmp(cleanup_arg, "verbatim"))
cleanup_mode = CLEANUP_NONE;
cleanup_mode = COMMIT_MSG_CLEANUP_NONE;
else if (!strcmp(cleanup_arg, "whitespace"))
cleanup_mode = CLEANUP_SPACE;
cleanup_mode = COMMIT_MSG_CLEANUP_SPACE;
else if (!strcmp(cleanup_arg, "strip"))
cleanup_mode = CLEANUP_ALL;
cleanup_mode = COMMIT_MSG_CLEANUP_ALL;
else if (!strcmp(cleanup_arg, "scissors"))
cleanup_mode = use_editor ? CLEANUP_SCISSORS : CLEANUP_SPACE;
cleanup_mode = use_editor ? COMMIT_MSG_CLEANUP_SCISSORS :
COMMIT_MSG_CLEANUP_SPACE;
else
die(_("Invalid cleanup mode %s"), cleanup_arg);
@ -1439,98 +1349,6 @@ int cmd_status(int argc, const char **argv, const char *prefix)
return 0;
}
static const char *implicit_ident_advice(void)
{
char *user_config = expand_user_path("~/.gitconfig", 0);
char *xdg_config = xdg_config_home("config");
int config_exists = file_exists(user_config) || file_exists(xdg_config);
free(user_config);
free(xdg_config);
if (config_exists)
return _(implicit_ident_advice_config);
else
return _(implicit_ident_advice_noconfig);
}
static void print_summary(const char *prefix, const struct object_id *oid,
int initial_commit)
{
struct rev_info rev;
struct commit *commit;
struct strbuf format = STRBUF_INIT;
const char *head;
struct pretty_print_context pctx = {0};
struct strbuf author_ident = STRBUF_INIT;
struct strbuf committer_ident = STRBUF_INIT;
commit = lookup_commit(oid);
if (!commit)
die(_("couldn't look up newly created commit"));
if (parse_commit(commit))
die(_("could not parse newly created commit"));
strbuf_addstr(&format, "format:%h] %s");
format_commit_message(commit, "%an <%ae>", &author_ident, &pctx);
format_commit_message(commit, "%cn <%ce>", &committer_ident, &pctx);
if (strbuf_cmp(&author_ident, &committer_ident)) {
strbuf_addstr(&format, "\n Author: ");
strbuf_addbuf_percentquote(&format, &author_ident);
}
if (author_date_is_interesting()) {
struct strbuf date = STRBUF_INIT;
format_commit_message(commit, "%ad", &date, &pctx);
strbuf_addstr(&format, "\n Date: ");
strbuf_addbuf_percentquote(&format, &date);
strbuf_release(&date);
}
if (!committer_ident_sufficiently_given()) {
strbuf_addstr(&format, "\n Committer: ");
strbuf_addbuf_percentquote(&format, &committer_ident);
if (advice_implicit_identity) {
strbuf_addch(&format, '\n');
strbuf_addstr(&format, implicit_ident_advice());
}
}
strbuf_release(&author_ident);
strbuf_release(&committer_ident);
init_revisions(&rev, prefix);
setup_revisions(0, NULL, &rev, NULL);
rev.diff = 1;
rev.diffopt.output_format =
DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY;
rev.verbose_header = 1;
rev.show_root_diff = 1;
get_commit_format(format.buf, &rev);
rev.always_show_header = 0;
rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
rev.diffopt.break_opt = 0;
diff_setup_done(&rev.diffopt);
head = resolve_ref_unsafe("HEAD", 0, NULL, NULL);
if (!head)
die_errno(_("unable to resolve HEAD after creating commit"));
if (!strcmp(head, "HEAD"))
head = _("detached HEAD");
else
skip_prefix(head, "refs/heads/", &head);
printf("[%s%s ", head, initial_commit ? _(" (root-commit)") : "");
if (!log_tree_commit(&rev, commit)) {
rev.always_show_header = 1;
rev.use_terminator = 1;
log_tree_commit(&rev, commit);
}
strbuf_release(&format);
}
static int git_commit_config(const char *k, const char *v, void *cb)
{
struct wt_status *s = cb;
@ -1560,37 +1378,6 @@ static int git_commit_config(const char *k, const char *v, void *cb)
return git_status_config(k, v, s);
}
static int run_rewrite_hook(const struct object_id *oldoid,
const struct object_id *newoid)
{
struct child_process proc = CHILD_PROCESS_INIT;
const char *argv[3];
int code;
struct strbuf sb = STRBUF_INIT;
argv[0] = find_hook("post-rewrite");
if (!argv[0])
return 0;
argv[1] = "amend";
argv[2] = NULL;
proc.argv = argv;
proc.in = -1;
proc.stdout_to_stderr = 1;
code = start_command(&proc);
if (code)
return code;
strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid));
sigchain_push(SIGPIPE, SIG_IGN);
write_in_full(proc.in, sb.buf, sb.len);
close(proc.in);
strbuf_release(&sb);
sigchain_pop(SIGPIPE);
return finish_command(&proc);
}
int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...)
{
struct argv_array hook_env = ARGV_ARRAY_INIT;
@ -1673,13 +1460,11 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
struct strbuf sb = STRBUF_INIT;
struct strbuf author_ident = STRBUF_INIT;
const char *index_file, *reflog_msg;
char *nl;
struct object_id oid;
struct commit_list *parents = NULL;
struct stat statbuf;
struct commit *current_head = NULL;
struct commit_extra_header *extra = NULL;
struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT;
if (argc == 2 && !strcmp(argv[1], "-h"))
@ -1770,17 +1555,17 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
}
if (verbose || /* Truncate the message just before the diff, if any. */
cleanup_mode == CLEANUP_SCISSORS)
cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS)
strbuf_setlen(&sb, wt_status_locate_end(sb.buf, sb.len));
if (cleanup_mode != CLEANUP_NONE)
strbuf_stripspace(&sb, cleanup_mode == CLEANUP_ALL);
if (cleanup_mode != COMMIT_MSG_CLEANUP_NONE)
strbuf_stripspace(&sb, cleanup_mode == COMMIT_MSG_CLEANUP_ALL);
if (message_is_empty(&sb) && !allow_empty_message) {
if (message_is_empty(&sb, cleanup_mode) && !allow_empty_message) {
rollback_index_files();
fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
exit(1);
}
if (template_untouched(&sb) && !allow_empty_message) {
if (template_untouched(&sb, template_file, cleanup_mode) && !allow_empty_message) {
rollback_index_files();
fprintf(stderr, _("Aborting commit; you did not edit the message.\n"));
exit(1);
@ -1802,25 +1587,11 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
strbuf_release(&author_ident);
free_commit_extra_headers(extra);
nl = strchr(sb.buf, '\n');
if (nl)
strbuf_setlen(&sb, nl + 1 - sb.buf);
else
strbuf_addch(&sb, '\n');
strbuf_insert(&sb, 0, reflog_msg, strlen(reflog_msg));
strbuf_insert(&sb, strlen(reflog_msg), ": ", 2);
transaction = ref_transaction_begin(&err);
if (!transaction ||
ref_transaction_update(transaction, "HEAD", &oid,
current_head
? &current_head->object.oid : &null_oid,
0, sb.buf, &err) ||
ref_transaction_commit(transaction, &err)) {
if (update_head_with_reflog(current_head, &oid, reflog_msg, &sb,
&err)) {
rollback_index_files();
die("%s", err.buf);
}
ref_transaction_free(transaction);
unlink(git_path_cherry_pick_head());
unlink(git_path_revert_head());
@ -1837,17 +1608,17 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
rerere(0);
run_commit_hook(use_editor, get_index_file(), "post-commit", NULL);
if (amend && !no_post_rewrite) {
struct notes_rewrite_cfg *cfg;
cfg = init_copy_notes_for_rewrite("amend");
if (cfg) {
/* we are amending, so current_head is not NULL */
copy_note_for_rewrite(cfg, &current_head->object.oid, &oid);
finish_copy_notes_for_rewrite(cfg, "Notes added by 'git commit --amend'");
}
run_rewrite_hook(&current_head->object.oid, &oid);
commit_post_rewrite(current_head, &oid);
}
if (!quiet) {
unsigned int flags = 0;
if (!current_head)
flags |= SUMMARY_INITIAL_COMMIT;
if (author_date_is_interesting())
flags |= SUMMARY_SHOW_AUTHOR_DATE;
print_commit_summary(prefix, &oid, flags);
}
if (!quiet)
print_summary(prefix, &oid, !current_head);
UNLEAK(err);
UNLEAK(sb);

View File

@ -43,7 +43,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
OPT_END()
};
git_config(git_default_config, NULL);
sequencer_init_config(&opts);
git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands);
opts.action = REPLAY_INTERACTIVE_REBASE;

View File

@ -208,7 +208,7 @@ int cmd_revert(int argc, const char **argv, const char *prefix)
if (isatty(0))
opts.edit = 1;
opts.action = REPLAY_REVERT;
git_config(git_default_config, NULL);
sequencer_init_config(&opts);
res = run_sequencer(argc, argv, &opts);
if (res < 0)
die(_("revert failed"));
@ -221,7 +221,7 @@ int cmd_cherry_pick(int argc, const char **argv, const char *prefix)
int res;
opts.action = REPLAY_PICK;
git_config(git_default_config, NULL);
sequencer_init_config(&opts);
res = run_sequencer(argc, argv, &opts);
if (res < 0)
die(_("cherry-pick failed"));

View File

@ -1,10 +1,10 @@
#include "cache.h"
#include "config.h"
#include "lockfile.h"
#include "sequencer.h"
#include "dir.h"
#include "object.h"
#include "commit.h"
#include "sequencer.h"
#include "tag.h"
#include "run-command.h"
#include "exec_cmd.h"
@ -21,12 +21,16 @@
#include "log-tree.h"
#include "wt-status.h"
#include "hashmap.h"
#include "notes-utils.h"
#include "sigchain.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
const char sign_off_header[] = "Signed-off-by: ";
static const char cherry_picked_prefix[] = "(cherry picked from commit ";
GIT_PATH_FUNC(git_path_commit_editmsg, "COMMIT_EDITMSG")
GIT_PATH_FUNC(git_path_seq_dir, "sequencer")
static GIT_PATH_FUNC(git_path_todo_file, "sequencer/todo")
@ -130,6 +134,51 @@ static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy")
static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts")
static GIT_PATH_FUNC(rebase_path_allow_rerere_autoupdate, "rebase-merge/allow_rerere_autoupdate")
static int git_sequencer_config(const char *k, const char *v, void *cb)
{
struct replay_opts *opts = cb;
int status;
if (!strcmp(k, "commit.cleanup")) {
const char *s;
status = git_config_string(&s, k, v);
if (status)
return status;
if (!strcmp(s, "verbatim"))
opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_NONE;
else if (!strcmp(s, "whitespace"))
opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_SPACE;
else if (!strcmp(s, "strip"))
opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_ALL;
else if (!strcmp(s, "scissors"))
opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_SPACE;
else
warning(_("invalid commit message cleanup mode '%s'"),
s);
return status;
}
if (!strcmp(k, "commit.gpgsign")) {
opts->gpg_sign = git_config_bool(k, v) ? xstrdup("") : NULL;
return 0;
}
status = git_gpg_config(k, v, NULL);
if (status)
return status;
return git_diff_basic_config(k, v, NULL);
}
void sequencer_init_config(struct replay_opts *opts)
{
opts->default_msg_cleanup = COMMIT_MSG_CLEANUP_NONE;
git_config(git_sequencer_config, opts);
}
static inline int is_rebase_i(const struct replay_opts *opts)
{
return opts->action == REPLAY_INTERACTIVE_REBASE;
@ -478,9 +527,6 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
_(action_name(opts)));
rollback_lock_file(&index_lock);
if (opts->signoff)
append_signoff(msgbuf, 0, 0);
if (!clean)
append_conflicts_hint(msgbuf);
@ -596,6 +642,18 @@ static int read_env_script(struct argv_array *env)
return 0;
}
static char *get_author(const char *message)
{
size_t len;
const char *a;
a = find_commit_header(message, "author", &len);
if (a)
return xmemdupz(a, len);
return NULL;
}
static const char staged_changes_advice[] =
N_("you have staged changes in your working tree\n"
"If these changes are meant to be squashed into the previous commit, run:\n"
@ -658,8 +716,6 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
argv_array_push(&cmd.args, "--amend");
if (opts->gpg_sign)
argv_array_pushf(&cmd.args, "-S%s", opts->gpg_sign);
if (opts->signoff)
argv_array_push(&cmd.args, "-s");
if (defmsg)
argv_array_pushl(&cmd.args, "-F", defmsg, NULL);
if ((flags & CLEANUP_MSG))
@ -694,6 +750,461 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts,
return run_command(&cmd);
}
static int rest_is_empty(const struct strbuf *sb, int start)
{
int i, eol;
const char *nl;
/* Check if the rest is just whitespace and Signed-off-by's. */
for (i = start; i < sb->len; i++) {
nl = memchr(sb->buf + i, '\n', sb->len - i);
if (nl)
eol = nl - sb->buf;
else
eol = sb->len;
if (strlen(sign_off_header) <= eol - i &&
starts_with(sb->buf + i, sign_off_header)) {
i = eol;
continue;
}
while (i < eol)
if (!isspace(sb->buf[i++]))
return 0;
}
return 1;
}
/*
* Find out if the message in the strbuf contains only whitespace and
* Signed-off-by lines.
*/
int message_is_empty(const struct strbuf *sb,
enum commit_msg_cleanup_mode cleanup_mode)
{
if (cleanup_mode == COMMIT_MSG_CLEANUP_NONE && sb->len)
return 0;
return rest_is_empty(sb, 0);
}
/*
* See if the user edited the message in the editor or left what
* was in the template intact
*/
int template_untouched(const struct strbuf *sb, const char *template_file,
enum commit_msg_cleanup_mode cleanup_mode)
{
struct strbuf tmpl = STRBUF_INIT;
const char *start;
if (cleanup_mode == COMMIT_MSG_CLEANUP_NONE && sb->len)
return 0;
if (!template_file || strbuf_read_file(&tmpl, template_file, 0) <= 0)
return 0;
strbuf_stripspace(&tmpl, cleanup_mode == COMMIT_MSG_CLEANUP_ALL);
if (!skip_prefix(sb->buf, tmpl.buf, &start))
start = sb->buf;
strbuf_release(&tmpl);
return rest_is_empty(sb, start - sb->buf);
}
int update_head_with_reflog(const struct commit *old_head,
const struct object_id *new_head,
const char *action, const struct strbuf *msg,
struct strbuf *err)
{
struct ref_transaction *transaction;
struct strbuf sb = STRBUF_INIT;
const char *nl;
int ret = 0;
if (action) {
strbuf_addstr(&sb, action);
strbuf_addstr(&sb, ": ");
}
nl = strchr(msg->buf, '\n');
if (nl) {
strbuf_add(&sb, msg->buf, nl + 1 - msg->buf);
} else {
strbuf_addbuf(&sb, msg);
strbuf_addch(&sb, '\n');
}
transaction = ref_transaction_begin(err);
if (!transaction ||
ref_transaction_update(transaction, "HEAD", new_head,
old_head ? &old_head->object.oid : &null_oid,
0, sb.buf, err) ||
ref_transaction_commit(transaction, err)) {
ret = -1;
}
ref_transaction_free(transaction);
strbuf_release(&sb);
return ret;
}
static int run_rewrite_hook(const struct object_id *oldoid,
const struct object_id *newoid)
{
struct child_process proc = CHILD_PROCESS_INIT;
const char *argv[3];
int code;
struct strbuf sb = STRBUF_INIT;
argv[0] = find_hook("post-rewrite");
if (!argv[0])
return 0;
argv[1] = "amend";
argv[2] = NULL;
proc.argv = argv;
proc.in = -1;
proc.stdout_to_stderr = 1;
code = start_command(&proc);
if (code)
return code;
strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid));
sigchain_push(SIGPIPE, SIG_IGN);
write_in_full(proc.in, sb.buf, sb.len);
close(proc.in);
strbuf_release(&sb);
sigchain_pop(SIGPIPE);
return finish_command(&proc);
}
void commit_post_rewrite(const struct commit *old_head,
const struct object_id *new_head)
{
struct notes_rewrite_cfg *cfg;
cfg = init_copy_notes_for_rewrite("amend");
if (cfg) {
/* we are amending, so old_head is not NULL */
copy_note_for_rewrite(cfg, &old_head->object.oid, new_head);
finish_copy_notes_for_rewrite(cfg, "Notes added by 'git commit --amend'");
}
run_rewrite_hook(&old_head->object.oid, new_head);
}
static int run_prepare_commit_msg_hook(struct strbuf *msg, const char *commit)
{
struct argv_array hook_env = ARGV_ARRAY_INIT;
int ret;
const char *name;
name = git_path_commit_editmsg();
if (write_message(msg->buf, msg->len, name, 0))
return -1;
argv_array_pushf(&hook_env, "GIT_INDEX_FILE=%s", get_index_file());
argv_array_push(&hook_env, "GIT_EDITOR=:");
if (commit)
ret = run_hook_le(hook_env.argv, "prepare-commit-msg", name,
"commit", commit, NULL);
else
ret = run_hook_le(hook_env.argv, "prepare-commit-msg", name,
"message", NULL);
if (ret)
ret = error(_("'prepare-commit-msg' hook failed"));
argv_array_clear(&hook_env);
return ret;
}
static const char implicit_ident_advice_noconfig[] =
N_("Your name and email address were configured automatically based\n"
"on your username and hostname. Please check that they are accurate.\n"
"You can suppress this message by setting them explicitly. Run the\n"
"following command and follow the instructions in your editor to edit\n"
"your configuration file:\n"
"\n"
" git config --global --edit\n"
"\n"
"After doing this, you may fix the identity used for this commit with:\n"
"\n"
" git commit --amend --reset-author\n");
static const char implicit_ident_advice_config[] =
N_("Your name and email address were configured automatically based\n"
"on your username and hostname. Please check that they are accurate.\n"
"You can suppress this message by setting them explicitly:\n"
"\n"
" git config --global user.name \"Your Name\"\n"
" git config --global user.email you@example.com\n"
"\n"
"After doing this, you may fix the identity used for this commit with:\n"
"\n"
" git commit --amend --reset-author\n");
static const char *implicit_ident_advice(void)
{
char *user_config = expand_user_path("~/.gitconfig", 0);
char *xdg_config = xdg_config_home("config");
int config_exists = file_exists(user_config) || file_exists(xdg_config);
free(user_config);
free(xdg_config);
if (config_exists)
return _(implicit_ident_advice_config);
else
return _(implicit_ident_advice_noconfig);
}
void print_commit_summary(const char *prefix, const struct object_id *oid,
unsigned int flags)
{
struct rev_info rev;
struct commit *commit;
struct strbuf format = STRBUF_INIT;
const char *head;
struct pretty_print_context pctx = {0};
struct strbuf author_ident = STRBUF_INIT;
struct strbuf committer_ident = STRBUF_INIT;
commit = lookup_commit(oid);
if (!commit)
die(_("couldn't look up newly created commit"));
if (parse_commit(commit))
die(_("could not parse newly created commit"));
strbuf_addstr(&format, "format:%h] %s");
format_commit_message(commit, "%an <%ae>", &author_ident, &pctx);
format_commit_message(commit, "%cn <%ce>", &committer_ident, &pctx);
if (strbuf_cmp(&author_ident, &committer_ident)) {
strbuf_addstr(&format, "\n Author: ");
strbuf_addbuf_percentquote(&format, &author_ident);
}
if (flags & SUMMARY_SHOW_AUTHOR_DATE) {
struct strbuf date = STRBUF_INIT;
format_commit_message(commit, "%ad", &date, &pctx);
strbuf_addstr(&format, "\n Date: ");
strbuf_addbuf_percentquote(&format, &date);
strbuf_release(&date);
}
if (!committer_ident_sufficiently_given()) {
strbuf_addstr(&format, "\n Committer: ");
strbuf_addbuf_percentquote(&format, &committer_ident);
if (advice_implicit_identity) {
strbuf_addch(&format, '\n');
strbuf_addstr(&format, implicit_ident_advice());
}
}
strbuf_release(&author_ident);
strbuf_release(&committer_ident);
init_revisions(&rev, prefix);
setup_revisions(0, NULL, &rev, NULL);
rev.diff = 1;
rev.diffopt.output_format =
DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY;
rev.verbose_header = 1;
rev.show_root_diff = 1;
get_commit_format(format.buf, &rev);
rev.always_show_header = 0;
rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
rev.diffopt.break_opt = 0;
diff_setup_done(&rev.diffopt);
head = resolve_ref_unsafe("HEAD", 0, NULL, NULL);
if (!head)
die_errno(_("unable to resolve HEAD after creating commit"));
if (!strcmp(head, "HEAD"))
head = _("detached HEAD");
else
skip_prefix(head, "refs/heads/", &head);
printf("[%s%s ", head, (flags & SUMMARY_INITIAL_COMMIT) ?
_(" (root-commit)") : "");
if (!log_tree_commit(&rev, commit)) {
rev.always_show_header = 1;
rev.use_terminator = 1;
log_tree_commit(&rev, commit);
}
strbuf_release(&format);
}
static int parse_head(struct commit **head)
{
struct commit *current_head;
struct object_id oid;
if (get_oid("HEAD", &oid)) {
current_head = NULL;
} else {
current_head = lookup_commit_reference(&oid);
if (!current_head)
return error(_("could not parse HEAD"));
if (oidcmp(&oid, &current_head->object.oid)) {
warning(_("HEAD %s is not a commit!"),
oid_to_hex(&oid));
}
if (parse_commit(current_head))
return error(_("could not parse HEAD commit"));
}
*head = current_head;
return 0;
}
/*
* Try to commit without forking 'git commit'. In some cases we need
* to run 'git commit' to display an error message
*
* Returns:
* -1 - error unable to commit
* 0 - success
* 1 - run 'git commit'
*/
static int try_to_commit(struct strbuf *msg, const char *author,
struct replay_opts *opts, unsigned int flags,
struct object_id *oid)
{
struct object_id tree;
struct commit *current_head;
struct commit_list *parents = NULL;
struct commit_extra_header *extra = NULL;
struct strbuf err = STRBUF_INIT;
struct strbuf commit_msg = STRBUF_INIT;
char *amend_author = NULL;
const char *hook_commit = NULL;
enum commit_msg_cleanup_mode cleanup;
int res = 0;
if (parse_head(&current_head))
return -1;
if (flags & AMEND_MSG) {
const char *exclude_gpgsig[] = { "gpgsig", NULL };
const char *out_enc = get_commit_output_encoding();
const char *message = logmsg_reencode(current_head, NULL,
out_enc);
if (!msg) {
const char *orig_message = NULL;
find_commit_subject(message, &orig_message);
msg = &commit_msg;
strbuf_addstr(msg, orig_message);
hook_commit = "HEAD";
}
author = amend_author = get_author(message);
unuse_commit_buffer(current_head, message);
if (!author) {
res = error(_("unable to parse commit author"));
goto out;
}
parents = copy_commit_list(current_head->parents);
extra = read_commit_extra_headers(current_head, exclude_gpgsig);
} else if (current_head) {
commit_list_insert(current_head, &parents);
}
if (write_cache_as_tree(tree.hash, 0, NULL)) {
res = error(_("git write-tree failed to write a tree"));
goto out;
}
if (!(flags & ALLOW_EMPTY) && !oidcmp(current_head ?
&current_head->tree->object.oid :
&empty_tree_oid, &tree)) {
res = 1; /* run 'git commit' to display error message */
goto out;
}
if (find_hook("prepare-commit-msg")) {
res = run_prepare_commit_msg_hook(msg, hook_commit);
if (res)
goto out;
if (strbuf_read_file(&commit_msg, git_path_commit_editmsg(),
2048) < 0) {
res = error_errno(_("unable to read commit message "
"from '%s'"),
git_path_commit_editmsg());
goto out;
}
msg = &commit_msg;
}
cleanup = (flags & CLEANUP_MSG) ? COMMIT_MSG_CLEANUP_ALL :
opts->default_msg_cleanup;
if (cleanup != COMMIT_MSG_CLEANUP_NONE)
strbuf_stripspace(msg, cleanup == COMMIT_MSG_CLEANUP_ALL);
if (!opts->allow_empty_message && message_is_empty(msg, cleanup)) {
res = 1; /* run 'git commit' to display error message */
goto out;
}
if (commit_tree_extended(msg->buf, msg->len, tree.hash, parents,
oid->hash, author, opts->gpg_sign, extra)) {
res = error(_("failed to write commit object"));
goto out;
}
if (update_head_with_reflog(current_head, oid,
getenv("GIT_REFLOG_ACTION"), msg, &err)) {
res = error("%s", err.buf);
goto out;
}
if (flags & AMEND_MSG)
commit_post_rewrite(current_head, oid);
out:
free_commit_extra_headers(extra);
strbuf_release(&err);
strbuf_release(&commit_msg);
free(amend_author);
return res;
}
static int do_commit(const char *msg_file, const char *author,
struct replay_opts *opts, unsigned int flags)
{
int res = 1;
if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
struct object_id oid;
struct strbuf sb = STRBUF_INIT;
if (msg_file && strbuf_read_file(&sb, msg_file, 2048) < 0)
return error_errno(_("unable to read commit message "
"from '%s'"),
msg_file);
res = try_to_commit(msg_file ? &sb : NULL, author, opts, flags,
&oid);
strbuf_release(&sb);
if (!res) {
unlink(git_path_cherry_pick_head());
unlink(git_path_merge_msg());
if (!is_rebase_i(opts))
print_commit_summary(NULL, &oid,
SUMMARY_SHOW_AUTHOR_DATE);
return res;
}
}
if (res == 1)
return run_git_commit(msg_file, opts, flags);
return res;
}
static int is_original_commit_empty(struct commit *commit)
{
const struct object_id *ptree_oid;
@ -952,6 +1463,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
struct object_id head;
struct commit *base, *next, *parent;
const char *base_label, *next_label;
char *author = NULL;
struct commit_message msg = { NULL, NULL, NULL, NULL };
struct strbuf msgbuf = STRBUF_INIT;
int res, unborn = 0, allow;
@ -1066,6 +1578,8 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
strbuf_addstr(&msgbuf, oid_to_hex(&commit->object.oid));
strbuf_addstr(&msgbuf, ")\n");
}
if (!is_fixup(command))
author = get_author(msg.message);
}
if (command == TODO_REWORD)
@ -1091,6 +1605,9 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
}
}
if (opts->signoff)
append_signoff(&msgbuf, 0, 0);
if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
res = -1;
else if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) {
@ -1148,9 +1665,13 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
goto leave;
} else if (allow)
flags |= ALLOW_EMPTY;
if (!opts->no_commit)
if (!opts->no_commit) {
fast_forward_edit:
res = run_git_commit(msg_file, opts, flags);
if (author || command == TODO_REVERT || (flags & AMEND_MSG))
res = do_commit(msg_file, author, opts, flags);
else
res = error(_("unable to parse commit author"));
}
if (!res && final_fixup) {
unlink(rebase_path_fixup_msg());
@ -1159,6 +1680,7 @@ fast_forward_edit:
leave:
free_message(commit, &msg);
free(author);
update_abort_safety_file();
return res;

View File

@ -1,6 +1,7 @@
#ifndef SEQUENCER_H
#define SEQUENCER_H
const char *git_path_commit_editmsg(void);
const char *git_path_seq_dir(void);
#define APPEND_SIGNOFF_DEDUP (1u << 0)
@ -11,6 +12,13 @@ enum replay_action {
REPLAY_INTERACTIVE_REBASE
};
enum commit_msg_cleanup_mode {
COMMIT_MSG_CLEANUP_SPACE,
COMMIT_MSG_CLEANUP_NONE,
COMMIT_MSG_CLEANUP_SCISSORS,
COMMIT_MSG_CLEANUP_ALL
};
struct replay_opts {
enum replay_action action;
@ -29,6 +37,7 @@ struct replay_opts {
int mainline;
char *gpg_sign;
enum commit_msg_cleanup_mode default_msg_cleanup;
/* Merge strategy */
char *strategy;
@ -40,6 +49,8 @@ struct replay_opts {
};
#define REPLAY_OPTS_INIT { -1 }
/* Call this to setup defaults before parsing command line options */
void sequencer_init_config(struct replay_opts *opts);
int sequencer_pick_revisions(struct replay_opts *opts);
int sequencer_continue(struct replay_opts *opts);
int sequencer_rollback(struct replay_opts *opts);
@ -61,5 +72,19 @@ extern const char sign_off_header[];
void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag);
void append_conflicts_hint(struct strbuf *msgbuf);
int message_is_empty(const struct strbuf *sb,
enum commit_msg_cleanup_mode cleanup_mode);
int template_untouched(const struct strbuf *sb, const char *template_file,
enum commit_msg_cleanup_mode cleanup_mode);
int update_head_with_reflog(const struct commit *old_head,
const struct object_id *new_head,
const char *action, const struct strbuf *msg,
struct strbuf *err);
void commit_post_rewrite(const struct commit *current_head,
const struct object_id *new_head);
#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
void print_commit_summary(const char *prefix, const struct object_id *oid,
unsigned int flags);
#endif

View File

@ -453,6 +453,10 @@ test_expect_success C_LOCALE_OUTPUT 'squash and fixup generate correct log messa
git rebase -i $base &&
git cat-file commit HEAD | sed -e 1,/^\$/d > actual-squash-fixup &&
test_cmp expect-squash-fixup actual-squash-fixup &&
git cat-file commit HEAD@{2} |
grep "^# This is a combination of 3 commits\." &&
git cat-file commit HEAD@{3} |
grep "^# This is a combination of 2 commits\." &&
git checkout to-be-rebased &&
git branch -D squash-fixup
'
@ -1336,6 +1340,16 @@ test_expect_success 'editor saves as CR/LF' '
SQ="'"
test_expect_success 'rebase -i --gpg-sign=<key-id>' '
test_when_finished "test_might_fail git rebase --abort" &&
set_fake_editor &&
FAKE_LINES="edit 1" git rebase -i --gpg-sign="\"S I Gner\"" HEAD^ \
>out 2>err &&
test_i18ngrep "$SQ-S\"S I Gner\"$SQ" err
'
test_expect_success 'rebase -i --gpg-sign=<key-id> overrides commit.gpgSign' '
test_when_finished "test_might_fail git rebase --abort" &&
test_config commit.gpgsign true &&
set_fake_editor &&
FAKE_LINES="edit 1" git rebase -i --gpg-sign="\"S I Gner\"" HEAD^ \
>out 2>err &&

View File

@ -5,7 +5,6 @@ test_description='cherry-pick can handle submodules'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-submodule-update.sh
KNOWN_FAILURE_CHERRY_PICK_SEES_EMPTY_COMMIT=1
KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
test_submodule_switch "git cherry-pick"

View File

@ -25,7 +25,6 @@ git_revert () {
git revert HEAD
}
KNOWN_FAILURE_CHERRY_PICK_SEES_EMPTY_COMMIT=1
KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
test_submodule_switch "git_revert"

View File

@ -4,6 +4,38 @@ test_description='prepare-commit-msg hook'
. ./test-lib.sh
test_expect_success 'set up commits for rebasing' '
test_commit root &&
test_commit a a a &&
test_commit b b b &&
git checkout -b rebase-me root &&
test_commit rebase-a a aa &&
test_commit rebase-b b bb &&
for i in $(test_seq 1 13)
do
test_commit rebase-$i c $i
done &&
git checkout master &&
cat >rebase-todo <<-EOF
pick $(git rev-parse rebase-a)
pick $(git rev-parse rebase-b)
fixup $(git rev-parse rebase-1)
fixup $(git rev-parse rebase-2)
pick $(git rev-parse rebase-3)
fixup $(git rev-parse rebase-4)
squash $(git rev-parse rebase-5)
reword $(git rev-parse rebase-6)
squash $(git rev-parse rebase-7)
fixup $(git rev-parse rebase-8)
fixup $(git rev-parse rebase-9)
edit $(git rev-parse rebase-10)
squash $(git rev-parse rebase-11)
squash $(git rev-parse rebase-12)
edit $(git rev-parse rebase-13)
EOF
'
test_expect_success 'with no hook' '
echo "foo" > file &&
@ -31,17 +63,41 @@ mkdir -p "$HOOKDIR"
echo "#!$SHELL_PATH" > "$HOOK"
cat >> "$HOOK" <<'EOF'
if test "$2" = commit; then
source=$(git rev-parse "$3")
GIT_DIR=$(git rev-parse --git-dir)
if test -d "$GIT_DIR/rebase-merge"
then
rebasing=1
else
source=${2-default}
rebasing=0
fi
if test "$GIT_EDITOR" = :; then
sed -e "1s/.*/$source (no editor)/" "$1" > msg.tmp
get_last_cmd () {
tail -n1 "$GIT_DIR/rebase-merge/done" | {
read cmd id _
git log --pretty="[$cmd %s]" -n1 $id
}
}
if test "$2" = commit
then
if test $rebasing = 1
then
source="$3"
else
source=$(git rev-parse "$3")
fi
else
sed -e "1s/.*/$source/" "$1" > msg.tmp
source=${2-default}
fi
test "$GIT_EDITOR" = : && source="$source (no editor)"
if test $rebasing = 1
then
echo "$source $(get_last_cmd)" >"$1"
else
sed -e "1s/.*/$source/" "$1" >msg.tmp
mv msg.tmp "$1"
fi
mv msg.tmp "$1"
exit 0
EOF
chmod +x "$HOOK"
@ -156,6 +212,63 @@ test_expect_success 'with hook and editor (merge)' '
test "$(git log -1 --pretty=format:%s)" = "merge"
'
test_rebase () {
expect=$1 &&
mode=$2 &&
test_expect_$expect C_LOCALE_OUTPUT "with hook (rebase $mode)" '
test_when_finished "\
git rebase --abort
git checkout -f master
git branch -D tmp" &&
git checkout -b tmp rebase-me &&
GIT_SEQUENCE_EDITOR="cp rebase-todo" &&
GIT_EDITOR="\"$FAKE_EDITOR\"" &&
(
export GIT_SEQUENCE_EDITOR GIT_EDITOR &&
test_must_fail git rebase $mode b &&
echo x >a &&
git add a &&
test_must_fail git rebase --continue &&
echo x >b &&
git add b &&
git commit &&
git rebase --continue &&
echo y >a &&
git add a &&
git commit &&
git rebase --continue &&
echo y >b &&
git add b &&
git rebase --continue
) &&
if test $mode = -p # reword amended after pick
then
n=18
else
n=17
fi &&
git log --pretty=%s -g -n$n HEAD@{1} >actual &&
test_cmp "$TEST_DIRECTORY/t7505/expected-rebase$mode" actual
'
}
test_rebase success -i
test_rebase success -p
test_expect_success 'with hook (cherry-pick)' '
test_when_finished "git checkout -f master" &&
git checkout -B other b &&
git cherry-pick rebase-1 &&
test "$(git log -1 --pretty=format:%s)" = "message (no editor)"
'
test_expect_success 'with hook and editor (cherry-pick)' '
test_when_finished "git checkout -f master" &&
git checkout -B other b &&
git cherry-pick -e rebase-1 &&
test "$(git log -1 --pretty=format:%s)" = merge
'
cat > "$HOOK" <<'EOF'
#!/bin/sh
exit 1
@ -197,4 +310,11 @@ test_expect_success 'with failing hook (merge)' '
'
test_expect_success C_LOCALE_OUTPUT 'with failing hook (cherry-pick)' '
test_when_finished "git checkout -f master" &&
git checkout -B other b &&
test_must_fail git cherry-pick rebase-1 2>actual &&
test $(grep -c prepare-commit-msg actual) = 1
'
test_done

17
t/t7505/expected-rebase-i Normal file
View File

@ -0,0 +1,17 @@
message [edit rebase-13]
message (no editor) [edit rebase-13]
message [squash rebase-12]
message (no editor) [squash rebase-11]
default [edit rebase-10]
message (no editor) [edit rebase-10]
message [fixup rebase-9]
message (no editor) [fixup rebase-8]
message (no editor) [squash rebase-7]
message [reword rebase-6]
message [squash rebase-5]
message (no editor) [fixup rebase-4]
message (no editor) [pick rebase-3]
message (no editor) [fixup rebase-2]
message (no editor) [fixup rebase-1]
merge [pick rebase-b]
message [pick rebase-a]

18
t/t7505/expected-rebase-p Normal file
View File

@ -0,0 +1,18 @@
message [edit rebase-13]
message (no editor) [edit rebase-13]
message [squash rebase-12]
message (no editor) [squash rebase-11]
default [edit rebase-10]
message (no editor) [edit rebase-10]
message [fixup rebase-9]
message (no editor) [fixup rebase-8]
message (no editor) [squash rebase-7]
HEAD [reword rebase-6]
message (no editor) [reword rebase-6]
message [squash rebase-5]
message (no editor) [fixup rebase-4]
message (no editor) [pick rebase-3]
message (no editor) [fixup rebase-2]
message (no editor) [fixup rebase-1]
merge [pick rebase-b]
message [pick rebase-a]