sequencer: try to commit without forking 'git commit'

If the commit message does not need to be edited then create the commit without forking 'git commit'. Taking the best time of ten runs with a warm cache this reduces the time taken to cherry-pick 10 commits by 27% (from 282ms to 204ms), and the time taken by 'git rebase --continue' to pick 10 commits by 45% (from 386ms to 212ms) on my computer running linux. Some of greater saving for rebase is because it no longer wastes time creating the commit summary just to throw it away. The code to create the commit is based on builtin/commit.c. It is simplified as it doesn't have to deal with merges and modified so that it does not die but returns an error to make sure the sequencer exits cleanly, as it would when forking 'git commit' Even when not forking 'git commit' the commit message is written to a file and CHERRY_PICK_HEAD is created unnecessarily. This could be eliminated in future. I hacked up a version that does not write these files and just passed an strbuf (with the wrong message for fixup and squash commands) to do_commit() but I couldn't measure any significant time difference when running cherry-pick or rebase. I think eliminating the writes properly for rebase would require a bit of effort as the code would need to be restructured. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Nov 24, 2017 at 11:07 UTC 356ee4659bb551cd9464b317d691827276752c2d
1 file changed +176 -2
sequencer.c
+176 -2
@@ -592,6 +592,18 @@ static int read_env_script(struct argv_array *env)
592 return 0;
593 }
594
595 +static char *get_author(const char *message)
596 +{
597 + size_t len;
598 + const char *a;
599 +
600 + a = find_commit_header(message, "author", &len);
601 + if (a)
602 + return xmemdupz(a, len);
603 +
604 + return NULL;
605 +}
606 +
607 static const char staged_changes_advice[] =
608 N_("you have staged changes in your working tree\n"
609 "If these changes are meant to be squashed into the previous commit, run:\n"
@@ -984,6 +996,160 @@ void print_commit_summary(const char *prefix, const struct object_id *oid,
996 strbuf_release(&format);
997 }
998
999 +static int parse_head(struct commit **head)
1000 +{
1001 + struct commit *current_head;
1002 + struct object_id oid;
1003 +
1004 + if (get_oid("HEAD", &oid)) {
1005 + current_head = NULL;
1006 + } else {
1007 + current_head = lookup_commit_reference(&oid);
1008 + if (!current_head)
1009 + return error(_("could not parse HEAD"));
1010 + if (oidcmp(&oid, &current_head->object.oid)) {
1011 + warning(_("HEAD %s is not a commit!"),
1012 + oid_to_hex(&oid));
1013 + }
1014 + if (parse_commit(current_head))
1015 + return error(_("could not parse HEAD commit"));
1016 + }
1017 + *head = current_head;
1018 +
1019 + return 0;
1020 +}
1021 +
1022 +/*
1023 + * Try to commit without forking 'git commit'. In some cases we need
1024 + * to run 'git commit' to display an error message
1025 + *
1026 + * Returns:
1027 + * -1 - error unable to commit
1028 + * 0 - success
1029 + * 1 - run 'git commit'
1030 + */
1031 +static int try_to_commit(struct strbuf *msg, const char *author,
1032 + struct replay_opts *opts, unsigned int flags,
1033 + struct object_id *oid)
1034 +{
1035 + struct object_id tree;
1036 + struct commit *current_head;
1037 + struct commit_list *parents = NULL;
1038 + struct commit_extra_header *extra = NULL;
1039 + struct strbuf err = STRBUF_INIT;
1040 + struct strbuf amend_msg = STRBUF_INIT;
1041 + char *amend_author = NULL;
1042 + const char *gpg_sign;
1043 + enum commit_msg_cleanup_mode cleanup;
1044 + int res = 0;
1045 +
1046 + if (parse_head(&current_head))
1047 + return -1;
1048 +
1049 + if (flags & AMEND_MSG) {
1050 + const char *exclude_gpgsig[] = { "gpgsig", NULL };
1051 + const char *out_enc = get_commit_output_encoding();
1052 + const char *message = logmsg_reencode(current_head, NULL,
1053 + out_enc);
1054 +
1055 + if (!msg) {
1056 + const char *orig_message = NULL;
1057 +
1058 + find_commit_subject(message, &orig_message);
1059 + msg = &amend_msg;
1060 + strbuf_addstr(msg, orig_message);
1061 + }
1062 + author = amend_author = get_author(message);
1063 + unuse_commit_buffer(current_head, message);
1064 + if (!author) {
1065 + res = error(_("unable to parse commit author"));
1066 + goto out;
1067 + }
1068 + parents = copy_commit_list(current_head->parents);
1069 + extra = read_commit_extra_headers(current_head, exclude_gpgsig);
1070 + } else if (current_head) {
1071 + commit_list_insert(current_head, &parents);
1072 + }
1073 +
1074 + cleanup = (flags & CLEANUP_MSG) ? COMMIT_MSG_CLEANUP_ALL :
1075 + default_msg_cleanup;
1076 + if (cleanup != COMMIT_MSG_CLEANUP_NONE)
1077 + strbuf_stripspace(msg, cleanup == COMMIT_MSG_CLEANUP_ALL);
1078 + if (!opts->allow_empty_message && message_is_empty(msg, cleanup)) {
1079 + res = 1; /* run 'git commit' to display error message */
1080 + goto out;
1081 + }
1082 +
1083 + gpg_sign = opts->gpg_sign ? opts->gpg_sign : default_gpg_sign;
1084 +
1085 + if (write_cache_as_tree(tree.hash, 0, NULL)) {
1086 + res = error(_("git write-tree failed to write a tree"));
1087 + goto out;
1088 + }
1089 +
1090 + if (!(flags & ALLOW_EMPTY) && !oidcmp(current_head ?
1091 + &current_head->tree->object.oid :
1092 + &empty_tree_oid, &tree)) {
1093 + res = 1; /* run 'git commit' to display error message */
1094 + goto out;
1095 + }
1096 +
1097 + if (commit_tree_extended(msg->buf, msg->len, tree.hash, parents,
1098 + oid->hash, author, gpg_sign, extra)) {
1099 + res = error(_("failed to write commit object"));
1100 + goto out;
1101 + }
1102 +
1103 + if (update_head_with_reflog(current_head, oid,
1104 + getenv("GIT_REFLOG_ACTION"), msg, &err)) {
1105 + res = error("%s", err.buf);
1106 + goto out;
1107 + }
1108 +
1109 + if (flags & AMEND_MSG)
1110 + commit_post_rewrite(current_head, oid);
1111 +
1112 +out:
1113 + free_commit_extra_headers(extra);
1114 + strbuf_release(&err);
1115 + strbuf_release(&amend_msg);
1116 + free(amend_author);
1117 +
1118 + return res;
1119 +}
1120 +
1121 +static int do_commit(const char *msg_file, const char *author,
1122 + struct replay_opts *opts, unsigned int flags)
1123 +{
1124 + int res = 1;
1125 +
1126 + if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
1127 + struct object_id oid;
1128 + struct strbuf sb = STRBUF_INIT;
1129 +
1130 + if (msg_file && strbuf_read_file(&sb, msg_file, 2048) < 0)
1131 + return error_errno(_("unable to read commit message "
1132 + "from '%s'"),
1133 + msg_file);
1134 +
1135 + res = try_to_commit(msg_file ? &sb : NULL, author, opts, flags,
1136 + &oid);
1137 + strbuf_release(&sb);
1138 + if (!res) {
1139 + unlink(git_path_cherry_pick_head());
1140 + unlink(git_path_merge_msg());
1141 + if (!is_rebase_i(opts))
1142 + print_commit_summary(NULL, &oid,
1143 + SUMMARY_SHOW_AUTHOR_DATE);
1144 + return res;
1145 + }
1146 + }
1147 + if (res == 1)
1148 + return run_git_commit(msg_file, opts, flags);
1149 +
1150 + return res;
1151 +}
1152 +
1153 static int is_original_commit_empty(struct commit *commit)
1154 {
1155 const struct object_id *ptree_oid;
@@ -1235,6 +1401,7 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
1401 struct object_id head;
1402 struct commit *base, *next, *parent;
1403 const char *base_label, *next_label;
1404 + char *author = NULL;
1405 struct commit_message msg = { NULL, NULL, NULL, NULL };
1406 struct strbuf msgbuf = STRBUF_INIT;
1407 int res, unborn = 0, allow;
@@ -1351,6 +1518,8 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
1518 strbuf_addstr(&msgbuf, oid_to_hex(&commit->object.oid));
1519 strbuf_addstr(&msgbuf, ")\n");
1520 }
1521 + if (!is_fixup(command))
1522 + author = get_author(msg.message);
1523 }
1524
1525 if (command == TODO_REWORD)
@@ -1436,9 +1605,13 @@ static int do_pick_commit(enum todo_command command, struct commit *commit,
1605 goto leave;
1606 } else if (allow)
1607 flags |= ALLOW_EMPTY;
1439 - if (!opts->no_commit)
1608 + if (!opts->no_commit) {
1609 fast_forward_edit:
1441 - res = run_git_commit(msg_file, opts, flags);
1610 + if (author || command == TODO_REVERT || (flags & AMEND_MSG))
1611 + res = do_commit(msg_file, author, opts, flags);
1612 + else
1613 + res = error(_("unable to parse commit author"));
1614 + }
1615
1616 if (!res && final_fixup) {
1617 unlink(rebase_path_fixup_msg());
@@ -1447,6 +1620,7 @@ fast_forward_edit:
1620
1621 leave:
1622 free_message(commit, &msg);
1623 + free(author);
1624 update_abort_safety_file();
1625
1626 return res;