sequencer: introduce new commands to reset the revision

In the upcoming commits, we will teach the sequencer to rebase merges. This will be done in a very different way from the unfortunate design of `git rebase --preserve-merges` (which does not allow for reordering commits, or changing the branch topology). The main idea is to introduce new todo list commands, to support labeling the current revision with a given name, resetting the current revision to a previous state, and merging labeled revisions. This idea was developed in Git for Windows' Git garden shears (that are used to maintain Git for Windows' "thicket of branches" on top of upstream Git), and this patch is part of the effort to make it available to a wider audience, as well as to make the entire process more robust (by implementing it in a safe and portable language rather than a Unix shell script). This commit implements the commands to label, and to reset to, given revisions. The syntax is: label <name> reset <name> Internally, the `label <name>` command creates the ref `refs/rewritten/<name>`. This makes it possible to work with the labeled revisions interactively, or in a scripted fashion (e.g. via the todo list command `exec`). These temporary refs are removed upon sequencer_remove_state(), so that even a `git rebase --abort` cleans them up. We disallow '#' as label because that character will be used as separator in the upcoming `merge` command. Later in this patch series, we will mark the `refs/rewritten/` refs as worktree-local, to allow for interactive rebases to be run in parallel in worktrees linked to the same repository. As typos happen, a failed `label` or `reset` command will be rescheduled immediately. As the previous code to reschedule a command is embedded deeply in the pick/fixup/squash code path, we simply duplicate the few lines. This will allow us to extend the new code path easily for the upcoming `merge` command. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Apr 25, 2018 at 14:28 UTC 9055e401dd63a422a4401a454ef2a0e2ef727ace
2 files changed +208 -7
git-rebase--interactive.sh
+2
@@ -162,6 +162,8 @@ s, squash <commit> = use commit, but meld into previous commit
162 f, fixup <commit> = like \"squash\", but discard this commit's log message
163 x, exec <commit> = run command (the rest of the line) using shell
164 d, drop <commit> = remove commit
165 +l, label <label> = label current HEAD with a name
166 +t, reset <label> = reset HEAD to a label
167
168 These lines can be re-ordered; they are executed from top to bottom.
169 " | git stripspace --comment-lines >>"$todo"
sequencer.c
+206 -7
@@ -23,6 +23,8 @@
23 #include "hashmap.h"
24 #include "notes-utils.h"
25 #include "sigchain.h"
26 +#include "unpack-trees.h"
27 +#include "worktree.h"
28
29 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
30
@@ -120,6 +122,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
122 static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
123 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
124 "rebase-merge/rewritten-pending")
125 +
126 +/*
127 + * The path of the file listing refs that need to be deleted after the rebase
128 + * finishes. This is used by the `label` command to record the need for cleanup.
129 + */
130 +static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
131 +
132 /*
133 * The following files are written by git-rebase just after parsing the
134 * command-line (and are only consumed, not modified, by the sequencer).
@@ -245,18 +254,34 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
254
255 int sequencer_remove_state(struct replay_opts *opts)
256 {
248 - struct strbuf dir = STRBUF_INIT;
257 + struct strbuf buf = STRBUF_INIT;
258 int i;
259
260 + if (is_rebase_i(opts) &&
261 + strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
262 + char *p = buf.buf;
263 + while (*p) {
264 + char *eol = strchr(p, '\n');
265 + if (eol)
266 + *eol = '\0';
267 + if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
268 + warning(_("could not delete '%s'"), p);
269 + if (!eol)
270 + break;
271 + p = eol + 1;
272 + }
273 + }
274 +
275 free(opts->gpg_sign);
276 free(opts->strategy);
277 for (i = 0; i < opts->xopts_nr; i++)
278 free(opts->xopts[i]);
279 free(opts->xopts);
280
257 - strbuf_addstr(&dir, get_dir(opts));
258 - remove_dir_recursively(&dir, 0);
259 - strbuf_release(&dir);
281 + strbuf_reset(&buf);
282 + strbuf_addstr(&buf, get_dir(opts));
283 + remove_dir_recursively(&buf, 0);
284 + strbuf_release(&buf);
285
286 return 0;
287 }
@@ -1280,6 +1305,8 @@ enum todo_command {
1305 TODO_SQUASH,
1306 /* commands that do something else than handling a single commit */
1307 TODO_EXEC,
1308 + TODO_LABEL,
1309 + TODO_RESET,
1310 /* commands that do nothing but are counted for reporting progress */
1311 TODO_NOOP,
1312 TODO_DROP,
@@ -1298,6 +1325,8 @@ static struct {
1325 { 'f', "fixup" },
1326 { 's', "squash" },
1327 { 'x', "exec" },
1328 + { 'l', "label" },
1329 + { 't', "reset" },
1330 { 0, "noop" },
1331 { 'd', "drop" },
1332 { 0, NULL }
@@ -1803,7 +1832,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
1832 return error(_("missing arguments for %s"),
1833 command_to_string(item->command));
1834
1806 - if (item->command == TODO_EXEC) {
1835 + if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
1836 + item->command == TODO_RESET) {
1837 item->commit = NULL;
1838 item->arg = bol;
1839 item->arg_len = (int)(eol - bol);
@@ -2471,6 +2501,159 @@ static int do_exec(const char *command_line)
2501 return status;
2502 }
2503
2504 +static int safe_append(const char *filename, const char *fmt, ...)
2505 +{
2506 + va_list ap;
2507 + struct lock_file lock = LOCK_INIT;
2508 + int fd = hold_lock_file_for_update(&lock, filename,
2509 + LOCK_REPORT_ON_ERROR);
2510 + struct strbuf buf = STRBUF_INIT;
2511 +
2512 + if (fd < 0)
2513 + return -1;
2514 +
2515 + if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT) {
2516 + error_errno(_("could not read '%s'"), filename);
2517 + rollback_lock_file(&lock);
2518 + return -1;
2519 + }
2520 + strbuf_complete(&buf, '\n');
2521 + va_start(ap, fmt);
2522 + strbuf_vaddf(&buf, fmt, ap);
2523 + va_end(ap);
2524 +
2525 + if (write_in_full(fd, buf.buf, buf.len) < 0) {
2526 + error_errno(_("could not write to '%s'"), filename);
2527 + strbuf_release(&buf);
2528 + rollback_lock_file(&lock);
2529 + return -1;
2530 + }
2531 + if (commit_lock_file(&lock) < 0) {
2532 + strbuf_release(&buf);
2533 + rollback_lock_file(&lock);
2534 + return error(_("failed to finalize '%s'"), filename);
2535 + }
2536 +
2537 + strbuf_release(&buf);
2538 + return 0;
2539 +}
2540 +
2541 +static int do_label(const char *name, int len)
2542 +{
2543 + struct ref_store *refs = get_main_ref_store();
2544 + struct ref_transaction *transaction;
2545 + struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
2546 + struct strbuf msg = STRBUF_INIT;
2547 + int ret = 0;
2548 + struct object_id head_oid;
2549 +
2550 + if (len == 1 && *name == '#')
2551 + return error("Illegal label name: '%.*s'", len, name);
2552 +
2553 + strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
2554 + strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
2555 +
2556 + transaction = ref_store_transaction_begin(refs, &err);
2557 + if (!transaction) {
2558 + error("%s", err.buf);
2559 + ret = -1;
2560 + } else if (get_oid("HEAD", &head_oid)) {
2561 + error(_("could not read HEAD"));
2562 + ret = -1;
2563 + } else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
2564 + NULL, 0, msg.buf, &err) < 0 ||
2565 + ref_transaction_commit(transaction, &err)) {
2566 + error("%s", err.buf);
2567 + ret = -1;
2568 + }
2569 + ref_transaction_free(transaction);
2570 + strbuf_release(&err);
2571 + strbuf_release(&msg);
2572 +
2573 + if (!ret)
2574 + ret = safe_append(rebase_path_refs_to_delete(),
2575 + "%s\n", ref_name.buf);
2576 + strbuf_release(&ref_name);
2577 +
2578 + return ret;
2579 +}
2580 +
2581 +static const char *reflog_message(struct replay_opts *opts,
2582 + const char *sub_action, const char *fmt, ...);
2583 +
2584 +static int do_reset(const char *name, int len, struct replay_opts *opts)
2585 +{
2586 + struct strbuf ref_name = STRBUF_INIT;
2587 + struct object_id oid;
2588 + struct lock_file lock = LOCK_INIT;
2589 + struct tree_desc desc;
2590 + struct tree *tree;
2591 + struct unpack_trees_options unpack_tree_opts;
2592 + int ret = 0, i;
2593 +
2594 + if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
2595 + return -1;
2596 +
2597 + /* Determine the length of the label */
2598 + for (i = 0; i < len; i++)
2599 + if (isspace(name[i]))
2600 + len = i;
2601 +
2602 + strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
2603 + if (get_oid(ref_name.buf, &oid) &&
2604 + get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
2605 + error(_("could not read '%s'"), ref_name.buf);
2606 + rollback_lock_file(&lock);
2607 + strbuf_release(&ref_name);
2608 + return -1;
2609 + }
2610 +
2611 + memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
2612 + setup_unpack_trees_porcelain(&unpack_tree_opts, "reset");
2613 + unpack_tree_opts.head_idx = 1;
2614 + unpack_tree_opts.src_index = &the_index;
2615 + unpack_tree_opts.dst_index = &the_index;
2616 + unpack_tree_opts.fn = oneway_merge;
2617 + unpack_tree_opts.merge = 1;
2618 + unpack_tree_opts.update = 1;
2619 +
2620 + if (read_cache_unmerged()) {
2621 + rollback_lock_file(&lock);
2622 + strbuf_release(&ref_name);
2623 + return error_resolve_conflict(_(action_name(opts)));
2624 + }
2625 +
2626 + if (!fill_tree_descriptor(&desc, &oid)) {
2627 + error(_("failed to find tree of %s"), oid_to_hex(&oid));
2628 + rollback_lock_file(&lock);
2629 + free((void *)desc.buffer);
2630 + strbuf_release(&ref_name);
2631 + return -1;
2632 + }
2633 +
2634 + if (unpack_trees(1, &desc, &unpack_tree_opts)) {
2635 + rollback_lock_file(&lock);
2636 + free((void *)desc.buffer);
2637 + strbuf_release(&ref_name);
2638 + return -1;
2639 + }
2640 +
2641 + tree = parse_tree_indirect(&oid);
2642 + prime_cache_tree(&the_index, tree);
2643 +
2644 + if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
2645 + ret = error(_("could not write index"));
2646 + free((void *)desc.buffer);
2647 +
2648 + if (!ret)
2649 + ret = update_ref(reflog_message(opts, "reset", "'%.*s'",
2650 + len, name), "HEAD", &oid,
2651 + NULL, 0, UPDATE_REFS_MSG_ON_ERR);
2652 +
2653 + strbuf_release(&ref_name);
2654 + return ret;
2655 +}
2656 +
2657 static int is_final_fixup(struct todo_list *todo_list)
2658 {
2659 int i = todo_list->current;
@@ -2574,7 +2757,7 @@ N_("Could not execute the todo command\n"
2757
2758 static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
2759 {
2577 - int res = 0;
2760 + int res = 0, reschedule = 0;
2761
2762 setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
2763 if (opts->allow_ff)
@@ -2645,7 +2828,7 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
2828 intend_to_amend();
2829 return error_failed_squash(item->commit, opts,
2830 item->arg_len, item->arg);
2648 - } else if (res && is_rebase_i(opts))
2831 + } else if (res && is_rebase_i(opts) && item->commit)
2832 return res | error_with_patch(item->commit,
2833 item->arg, item->arg_len, opts, res,
2834 item->command == TODO_REWORD);
@@ -2671,9 +2854,25 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
2854 /* `current` will be incremented below */
2855 todo_list->current = -1;
2856 }
2857 + } else if (item->command == TODO_LABEL) {
2858 + if ((res = do_label(item->arg, item->arg_len)))
2859 + reschedule = 1;
2860 + } else if (item->command == TODO_RESET) {
2861 + if ((res = do_reset(item->arg, item->arg_len, opts)))
2862 + reschedule = 1;
2863 } else if (!is_noop(item->command))
2864 return error(_("unknown command %d"), item->command);
2865
2866 + if (reschedule) {
2867 + advise(_(rescheduled_advice),
2868 + get_item_line_length(todo_list,
2869 + todo_list->current),
2870 + get_item_line(todo_list, todo_list->current));
2871 + todo_list->current--;
2872 + if (save_todo(todo_list, opts))
2873 + return -1;
2874 + }
2875 +
2876 todo_list->current++;
2877 if (res)
2878 return res;