This patch series marks the '4' in the countdown to speed up rebase -i by implementing large parts in C. It is based on the `libify-sequencer` patch series that I submitted last week. The patches in this series merely prepare the sequencer code for the next patch series that actually teaches the sequencer to run an interactive rebase. The reason to split these two patch series is simple: to keep them at a sensible size. The two patch series after that are much smaller: a two-patch "series" that switches rebase -i to use the sequencer (except with --root or --preserve-merges), and a couple of patches to move several pretty expensive script processing steps to C (think: autosquash). The end game of this patch series is a git-rebase--helper that makes rebase -i 5x faster on Windows (according to t/perf/p3404). Travis says that even MacOSX and Linux benefit (4x and 3x, respectively). I have been working on this since early February, whenever time allowed, and it is time to put it into the users' hands. To that end, I will most likely submit the remaining three patch series in the next two days, and integrate the whole shebang into Git for Windows 2.10.0. Therefore I would be most grateful for every in-depth review. Changes vs v1: - clarified why the code refactoring into the short_commit_name() function is desirable. - fixed a typo in a commit message (s/se/so/). - removed a bogus call to read_and_refresh_cache(opts) that was added to sequencer_rollback() by mistake. - clarified in the commit message why the TODO_LIST_INIT macro does not assign all-zeroes. - converted IS_REBASE_I() into an inline function. - clarified the comment about retaining author metadata when calling sequencer_commit() in rebase -i mode. - simplified TODO_LIST_INIT and REPLAY_OPTS_INIT. - added an example how to use sequencer_entrust() to the commit message introducing it. - the gpg_sign option's value is now also entrusted to the sequencer (i.e. it is released in sequencer_remove_state()). - renamed the "set_me_free_after_use" to "to_free". Since that is still not a self-explanatory name, add a comment to the function declaration. - clarified in the "completely revamped todo parsing" commit message that the main benefit of the early parsing is for rebase -i. - start the `enum todo_command` with TODO_PICK that is explicitly assigned the value 0. - an error was marked as translatable. - converted one forgotten git_path_todo_file() to use get_todo_path(opts) instead. - fixed numbering of "malformed instruction sheet"s after removing one. - reintroduced the overzealous assumption that todo scripts can only perform revert commands during `git revert` and pick commands during `git cherry-pick`. This overzealous assumption is *still* disabled in rebase -i mode, of course. - clarified in the commit message why we call the field "arg", not "oneline", and fixed the description that claimed that we store the end offset (we store the length instead). - clarified in the commit message of "allow editing the commit message on a case-by-case basis" that we are talking about the sequencer_commit() function, and how things were done previously. - some grammar touch-ups. - marked a couple of error messages for translation. - explicitly assign the first todo_command the value 0, so that we can be certain that the array of command strings matches up. - removed code to skip CR/LF at the end of line after reading with read_oneliner(): That function already ensures that. - ensured that the todo_list is released even when reading/parsing fails. - replaced an error(..., strerror()) call with an error_errno(...) one. - clarified in the commit message why the new todo_list maintains the script as an array instead of a linked list. - renamed the append_todo() function into append_new_todo(), to explain better what the function does and why it does not take the values as parameters that should populate the new todo_item. - marked action_name() for translation. - surrounded all file names in error messages in sequencer.c with single quotes, for consistency. - removed a bogus hint for translators that asked them to translate commands of the git-rebase-todo file (or cherry-pick's equivalent). - turned capitals after semicolons to lower-case. Johannes Schindelin (25): sequencer: use static initializers for replay_opts sequencer: use memoized sequencer directory path sequencer: avoid unnecessary indirection sequencer: future-proof remove_sequencer_state() sequencer: allow the sequencer to take custody of malloc()ed data sequencer: release memory that was allocated when reading options sequencer: future-proof read_populate_todo() sequencer: completely revamp the "todo" script parsing sequencer: avoid completely different messages for different actions sequencer: get rid of the subcommand field sequencer: refactor the code to obtain a short commit name sequencer: remember the onelines when parsing the todo file sequencer: prepare for rebase -i's commit functionality sequencer: introduce a helper to read files written by scripts sequencer: prepare for rebase -i's GPG settings sequencer: allow editing the commit message on a case-by-case basis sequencer: support amending commits sequencer: support cleaning up commit messages sequencer: remember do_recursive_merge()'s return value sequencer: left-trim lines read from the script sequencer: refactor write_message() sequencer: remove overzealous assumption in rebase -i mode sequencer: mark action_name() for translation sequencer: quote filenames in error messages sequencer: remove bogus hint for translators builtin/commit.c | 2 +- builtin/revert.c | 42 ++-- sequencer.c | 632 +++++++++++++++++++++++++++++++++++++------------------ sequencer.h | 31 +-- 4 files changed, 468 insertions(+), 239 deletions(-) Based-On: libify-sequencer at https://github.com/dscho/git Fetch-Base-Via: git fetch https://github.com/dscho/git libify-sequencer Published-As: https://github.com/dscho/git/releases/tag/prepare-sequencer-v2 Fetch-It-Via: git fetch https://github.com/dscho/git prepare-sequencer-v2 Interdiff vs v1: diff --git a/sequencer.c b/sequencer.c index 7d56676..cdff0f1 100644 --- a/sequencer.c +++ b/sequencer.c @@ -41,7 +41,10 @@ static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt") /* We will introduce the 'interactive rebase' mode later */ -#define IS_REBASE_I() 0 +static inline int is_rebase_i(const struct replay_opts *opts) +{ + return 0; +} static const char *get_dir(const struct replay_opts *opts) { @@ -145,12 +148,12 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts) return buf.buf; } -void *sequencer_entrust(struct replay_opts *opts, void *set_me_free_after_use) +void *sequencer_entrust(struct replay_opts *opts, void *to_free) { ALLOC_GROW(opts->owned, opts->owned_nr + 1, opts->owned_alloc); - opts->owned[opts->owned_nr++] = set_me_free_after_use; + opts->owned[opts->owned_nr++] = to_free; - return set_me_free_after_use; + return to_free; } int sequencer_remove_state(struct replay_opts *opts) @@ -173,7 +176,7 @@ int sequencer_remove_state(struct replay_opts *opts) static const char *action_name(const struct replay_opts *opts) { - return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; + return opts->action == REPLAY_REVERT ? N_("revert") : N_("cherry-pick"); } struct commit_message { @@ -248,11 +251,11 @@ static int write_with_lock_file(const char *filename, if (msg_fd < 0) return error_errno(_("Could not lock '%s'"), filename); if (write_in_full(msg_fd, buf, len) < 0) - return error_errno(_("Could not write to %s"), filename); + return error_errno(_("Could not write to '%s'"), filename); if (append_eol && write(msg_fd, "\n", 1) < 0) - return error_errno(_("Could not write eol to %s"), filename); + return error_errno(_("Could not write eol to '%s"), filename); if (commit_lock_file(&msg_file) < 0) - return error(_("Error wrapping up %s."), filename); + return error(_("Error wrapping up '%s'."), filename); return 0; } @@ -309,10 +312,10 @@ static struct tree *empty_tree(void) static int error_dirty_index(struct replay_opts *opts) { if (read_cache_unmerged()) - return error_resolve_conflict(action_name(opts)); + return error_resolve_conflict(_(action_name(opts))); error(_("Your local changes would be overwritten by %s."), - action_name(opts)); + _(action_name(opts))); if (advice_commit_before_merge) advise(_("Commit your changes or stash them to proceed.")); @@ -330,7 +333,7 @@ static int fast_forward_to(const unsigned char *to, const unsigned char *from, if (checkout_fast_forward(from, to, 1)) return -1; /* the callee should have complained already */ - strbuf_addf(&sb, _("%s: fast-forward"), action_name(opts)); + strbuf_addf(&sb, _("%s: fast-forward"), _(action_name(opts))); transaction = ref_transaction_begin(&err); if (!transaction || @@ -406,7 +409,7 @@ static int do_recursive_merge(struct commit *base, struct commit *next, write_locked_index(&the_index, &index_lock, COMMIT_LOCK)) /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ return error(_("%s: Unable to write new index file"), - action_name(opts)); + _(action_name(opts))); rollback_lock_file(&index_lock); if (opts->signoff) @@ -488,9 +491,13 @@ static char **read_author_script(void) * If we are cherry-pick, and if the merge did not result in * hand-editing, we will hit this commit and inherit the original * author date and name. + * * If we are revert, or if our cherry-pick results in a hand merge, - * we had better say that the current user is responsible for that - * (except, of course, while running an interactive rebase). + * we had better say that the current user is responsible for that. + * + * An exception is when sequencer_commit() is called during an + * interactive rebase: in that case, we will want to retain the + * author metadata. */ int sequencer_commit(const char *defmsg, struct replay_opts *opts, int allow_empty, int edit, int amend, @@ -501,7 +508,7 @@ int sequencer_commit(const char *defmsg, struct replay_opts *opts, int rc; const char *value; - if (IS_REBASE_I()) { + if (is_rebase_i(opts)) { env = read_author_script(); if (!env) { const char *gpg_opt = gpg_sign_opt_quoted(opts); @@ -513,7 +520,7 @@ int sequencer_commit(const char *defmsg, struct replay_opts *opts, "If they are meant to go into a new commit, " "run:\n\n" " git commit %s\n\n" - "In both case, once you're done, continue " + "In both cases, once you're done, continue " "with:\n\n" " git rebase --continue\n", gpg_opt, gpg_opt); } @@ -613,7 +620,7 @@ static int allow_empty(struct replay_opts *opts, struct commit *commit) } enum todo_command { - TODO_PICK, + TODO_PICK = 0, TODO_REVERT }; @@ -690,8 +697,6 @@ static int do_pick_commit(enum todo_command command, struct commit *commit, return fast_forward_to(commit->object.oid.hash, head, unborn, opts); if (parent && parse_commit(parent) < 0) - /* TRANSLATORS: The first %s will be "revert" or - "cherry-pick", the second %s a SHA1 */ return error(_("%s: cannot parse parent commit %s"), command_to_string(command), oid_to_hex(&parent->object.oid)); @@ -833,14 +838,14 @@ static int read_and_refresh_cache(struct replay_opts *opts) if (read_index_preload(&the_index, NULL) < 0) { rollback_lock_file(&index_lock); return error(_("git %s: failed to read the index"), - action_name(opts)); + _(action_name(opts))); } refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL); if (the_index.cache_changed && index_fd >= 0) { if (write_locked_index(&the_index, &index_lock, COMMIT_LOCK)) { rollback_lock_file(&index_lock); return error(_("git %s: failed to refresh the index"), - action_name(opts)); + _(action_name(opts))); } } rollback_lock_file(&index_lock); @@ -861,7 +866,7 @@ struct todo_list { int nr, alloc, current; }; -#define TODO_LIST_INIT { STRBUF_INIT, NULL, 0, 0, 0 } +#define TODO_LIST_INIT { STRBUF_INIT } static void todo_list_release(struct todo_list *todo_list) { @@ -871,7 +876,7 @@ static void todo_list_release(struct todo_list *todo_list) todo_list->nr = todo_list->alloc = 0; } -struct todo_item *append_todo(struct todo_list *todo_list) +struct todo_item *append_new_todo(struct todo_list *todo_list) { ALLOC_GROW(todo_list->items, todo_list->nr + 1, todo_list->alloc); return todo_list->items + todo_list->nr++; @@ -925,11 +930,11 @@ static int parse_insn_buffer(char *buf, struct todo_list *todo_list) for (i = 1; *p; i++) { char *eol = strchrnul(p, '\n'); - item = append_todo(todo_list); + item = append_new_todo(todo_list); item->offset_in_buf = p - todo_list->buf.buf; if (parse_insn_line(item, p, eol)) { - error("Invalid line: %.*s", (int)(eol - p), p); - res |= error(_("Could not parse line %d."), i); + res |= error(_("Invalid line %d: %.*s"), + i, (int)(eol - p), p); item->command = -1; } p = *eol ? eol + 1 : eol; @@ -948,16 +953,31 @@ static int read_populate_todo(struct todo_list *todo_list, strbuf_reset(&todo_list->buf); fd = open(todo_file, O_RDONLY); if (fd < 0) - return error_errno(_("Could not open %s"), todo_file); + return error_errno(_("Could not open '%s'"), todo_file); if (strbuf_read(&todo_list->buf, fd, 0) < 0) { close(fd); - return error(_("Could not read %s."), todo_file); + return error(_("Could not read '%s'."), todo_file); } close(fd); res = parse_insn_buffer(todo_list->buf.buf, todo_list); if (res) - return error(_("Unusable instruction sheet: %s"), todo_file); + return error(_("Unusable instruction sheet: '%s'"), todo_file); + + if (!is_rebase_i(opts)) { + enum todo_command valid = + opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT; + int i; + + for (i = 0; i < todo_list->nr; i++) + if (valid == todo_list->items[i].command) + continue; + else if (valid == TODO_PICK) + return error(_("Cannot cherry-pick during a revert.")); + else + return error(_("Cannot revert during a cherry-pick.")); + } + return 0; } @@ -1003,22 +1023,16 @@ static int populate_opts_cb(const char *key, const char *value, void *data) static int read_populate_opts(struct replay_opts *opts) { - if (IS_REBASE_I()) { + if (is_rebase_i(opts)) { struct strbuf buf = STRBUF_INIT; if (read_oneliner(&buf, rebase_path_gpg_sign_opt(), 1)) { - if (buf.len && buf.buf[buf.len - 1] == '\n') { - if (--buf.len && - buf.buf[buf.len - 1] == '\r') - buf.len--; - buf.buf[buf.len] = '\0'; - } - if (!starts_with(buf.buf, "-S")) strbuf_reset(&buf); else { opts->gpg_sign = buf.buf + 2; - strbuf_detach(&buf, NULL); + sequencer_entrust(opts, + strbuf_detach(&buf, NULL)); } } @@ -1034,7 +1048,7 @@ static int read_populate_opts(struct replay_opts *opts) * are pretty certain that it is syntactically correct. */ if (git_config_from_file(populate_opts_cb, git_path_opts_file(), opts) < 0) - return error(_("Malformed options sheet: %s"), + return error(_("Malformed options sheet: '%s'"), git_path_opts_file()); return 0; } @@ -1044,13 +1058,14 @@ static int walk_revs_populate_todo(struct todo_list *todo_list, { enum todo_command command = opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT; + const char *command_string = todo_command_strings[command]; struct commit *commit; if (prepare_revs(opts)) return -1; while ((commit = get_revision(opts->revs))) { - struct todo_item *item = append_todo(todo_list); + struct todo_item *item = append_new_todo(todo_list); const char *commit_buffer = get_commit_buffer(commit, NULL); const char *subject; int subject_len; @@ -1061,8 +1076,7 @@ static int walk_revs_populate_todo(struct todo_list *todo_list, item->arg_len = 0; item->offset_in_buf = todo_list->buf.len; subject_len = find_commit_subject(commit_buffer, &subject); - strbuf_addf(&todo_list->buf, "%s %s %.*s\n", - opts->action == REPLAY_PICK ? "pick" : "revert", + strbuf_addf(&todo_list->buf, "%s %s %.*s\n", command_string, short_commit_name(commit), subject_len, subject); unuse_commit_buffer(commit, commit_buffer); } @@ -1077,7 +1091,7 @@ static int create_seq_dir(void) return -1; } else if (mkdir(git_path_seq_dir(), 0777) < 0) - return error_errno(_("Could not create sequencer directory %s"), + return error_errno(_("Could not create sequencer directory '%s'"), git_path_seq_dir()); return 0; } @@ -1096,12 +1110,12 @@ static int save_head(const char *head) strbuf_addf(&buf, "%s\n", head); if (write_in_full(fd, buf.buf, buf.len) < 0) { rollback_lock_file(&head_lock); - return error_errno(_("Could not write to %s"), + return error_errno(_("Could not write to '%s'"), git_path_head_file()); } if (commit_lock_file(&head_lock) < 0) { rollback_lock_file(&head_lock); - return error(_("Error wrapping up %s."), git_path_head_file()); + return error(_("Error wrapping up '%s'."), git_path_head_file()); } return 0; } @@ -1136,9 +1150,6 @@ int sequencer_rollback(struct replay_opts *opts) unsigned char sha1[20]; struct strbuf buf = STRBUF_INIT; - if (read_and_refresh_cache(opts)) - return -1; - f = fopen(git_path_head_file(), "r"); if (!f && errno == ENOENT) { /* @@ -1149,9 +1160,9 @@ int sequencer_rollback(struct replay_opts *opts) return rollback_single_pick(); } if (!f) - return error_errno(_("cannot open %s"), git_path_head_file()); + return error_errno(_("cannot open '%s'"), git_path_head_file()); if (strbuf_getline_lf(&buf, f)) { - error(_("cannot read %s: %s"), git_path_head_file(), + error(_("cannot read '%s': %s"), git_path_head_file(), ferror(f) ? strerror(errno) : _("unexpected end of file")); fclose(f); goto fail; @@ -1183,16 +1194,14 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts) fd = hold_lock_file_for_update(&todo_lock, todo_path, 0); if (fd < 0) - return error_errno(_("Could not lock '%s'"), - git_path_todo_file()); + return error_errno(_("Could not lock '%s'"), todo_path); offset = next < todo_list->nr ? todo_list->items[next].offset_in_buf : todo_list->buf.len; if (write_in_full(fd, todo_list->buf.buf + offset, todo_list->buf.len - offset) < 0) - return error(_("Could not write to %s (%s)"), - todo_path, strerror(errno)); + return error_errno(_("Could not write to '%s'"), todo_path); if (commit_lock_file(&todo_lock) < 0) - return error(_("Error wrapping up %s."), todo_path); + return error(_("Error wrapping up '%s'."), todo_path); return 0; } @@ -1279,21 +1288,25 @@ int sequencer_continue(struct replay_opts *opts) if (!file_exists(get_todo_path(opts))) return continue_single_pick(); - if (read_populate_opts(opts) || - read_populate_todo(&todo_list, opts)) + if (read_populate_opts(opts)) return -1; + if ((res = read_populate_todo(&todo_list, opts))) + goto release_todo_list; /* Verify that the conflict has been resolved */ if (file_exists(git_path_cherry_pick_head()) || file_exists(git_path_revert_head())) { - int ret = continue_single_pick(); - if (ret) - return ret; + res = continue_single_pick(); + if (res) + goto release_todo_list; + } + if (index_differs_from("HEAD", 0)) { + res = error_dirty_index(opts); + goto release_todo_list; } - if (index_differs_from("HEAD", 0)) - return error_dirty_index(opts); todo_list.current++; res = pick_commits(&todo_list, opts); +release_todo_list: todo_list_release(&todo_list); return res; } diff --git a/sequencer.h b/sequencer.h index e272549..688fff1 100644 --- a/sequencer.h +++ b/sequencer.h @@ -40,9 +40,13 @@ struct replay_opts { void **owned; int owned_nr, owned_alloc; }; -#define REPLAY_OPTS_INIT { -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL, NULL, 0, 0, NULL, NULL, 0, 0 } +#define REPLAY_OPTS_INIT { -1 } -void *sequencer_entrust(struct replay_opts *opts, void *set_me_free_after_use); +/* + * Make it the duty of sequencer_remove_state() to release the memory; + * For ease of use, return the same pointer. + */ +void *sequencer_entrust(struct replay_opts *opts, void *to_free); int sequencer_pick_revisions(struct replay_opts *opts); int sequencer_continue(struct replay_opts *opts); diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh index 6465edf..7b7a89d 100755 --- a/t/t3510-cherry-pick-sequence.sh +++ b/t/t3510-cherry-pick-sequence.sh @@ -459,6 +459,17 @@ test_expect_success 'malformed instruction sheet 1' ' test_expect_code 128 git cherry-pick --continue ' +test_expect_success 'malformed instruction sheet 2' ' + pristine_detach initial && + test_expect_code 1 git cherry-pick base..anotherpick && + echo "resolved" >foo && + git add foo && + git commit && + sed "s/pick/revert/" .git/sequencer/todo >new_sheet && + cp new_sheet .git/sequencer/todo && + test_expect_code 128 git cherry-pick --continue +' + test_expect_success 'empty commit set' ' pristine_detach initial && test_expect_code 128 git cherry-pick base..base -- 2.10.0.windows.1.10.g803177d base-commit: 8a04d01a511ecffba8a397d26ee7fa6ca56b6e7e