revert: Save data for continuing after conflict resolution

Ever since v1.7.2-rc1~4^2~7 (revert: allow cherry-picking more than one commit, 2010-06-02), a single invocation of "git cherry-pick" or "git revert" can perform picks of several individual commits. To implement features like "--continue" to continue the whole operation, we will need to store some information about the state and the plan at the beginning. Introduce a ".git/sequencer/head" file to store this state, and ".git/sequencer/todo" file to store the plan. The head file contains the SHA-1 of the HEAD before the start of the operation, and the todo file contains an instruction sheet whose format is inspired by the format of the "rebase -i" instruction sheet. As a result, a typical todo file looks like: pick 8537f0e submodule add: test failure when url is not configured pick 4d68932 submodule add: allow relative repository path pick f22a17e submodule add: clean up duplicated code pick 59a5775 make copy_ref globally available Since SHA-1 hex is abbreviated using an find_unique_abbrev(), it is unambiguous. This does not guarantee that there will be no ambiguity when more objects are added to the repository. These two files alone are not enough to implement a "--continue" that remembers the command-line options specified; later patches in the series save them too. These new files are unrelated to the existing .git/CHERRY_PICK_HEAD, which will still be useful while committing after a conflict resolution. Inspired-by: Christian Couder <chriscool@tuxfamily.org> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Jonathan Nieder <jrnieder@gmail.com> Signed-off-by: Ramkumar Ramachandra <artagnon@gmail.com> Signed-off-by: Jonathan Nieder <jrnieder@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ramkumar Ramachandra committed Aug 4, 2011 at 16:09 UTC 04d3d3cfc4a26f003c5ae2b5598cc975a31e4395
2 files changed +178 -4
builtin/revert.c
+130 -4
@@ -13,6 +13,7 @@
13 #include "rerere.h"
14 #include "merge-recursive.h"
15 #include "refs.h"
16 +#include "dir.h"
17
18 /*
19 * This implements the builtins revert and cherry-pick.
@@ -60,6 +61,10 @@ struct replay_opts {
61
62 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
63
64 +#define SEQ_DIR "sequencer"
65 +#define SEQ_HEAD_FILE "sequencer/head"
66 +#define SEQ_TODO_FILE "sequencer/todo"
67 +
68 static const char *action_name(const struct replay_opts *opts)
69 {
70 return opts->action == REVERT ? "revert" : "cherry-pick";
@@ -587,10 +592,116 @@ static void read_and_refresh_cache(struct replay_opts *opts)
592 rollback_lock_file(&index_lock);
593 }
594
590 -static int pick_commits(struct replay_opts *opts)
595 +/*
596 + * Append a commit to the end of the commit_list.
597 + *
598 + * next starts by pointing to the variable that holds the head of an
599 + * empty commit_list, and is updated to point to the "next" field of
600 + * the last item on the list as new commits are appended.
601 + *
602 + * Usage example:
603 + *
604 + * struct commit_list *list;
605 + * struct commit_list **next = &list;
606 + *
607 + * next = commit_list_append(c1, next);
608 + * next = commit_list_append(c2, next);
609 + * assert(commit_list_count(list) == 2);
610 + * return list;
611 + */
612 +struct commit_list **commit_list_append(struct commit *commit,
613 + struct commit_list **next)
614 +{
615 + struct commit_list *new = xmalloc(sizeof(struct commit_list));
616 + new->item = commit;
617 + *next = new;
618 + new->next = NULL;
619 + return &new->next;
620 +}
621 +
622 +static int format_todo(struct strbuf *buf, struct commit_list *todo_list,
623 + struct replay_opts *opts)
624 +{
625 + struct commit_list *cur = NULL;
626 + struct commit_message msg = { NULL, NULL, NULL, NULL, NULL };
627 + const char *sha1_abbrev = NULL;
628 + const char *action_str = opts->action == REVERT ? "revert" : "pick";
629 +
630 + for (cur = todo_list; cur; cur = cur->next) {
631 + sha1_abbrev = find_unique_abbrev(cur->item->object.sha1, DEFAULT_ABBREV);
632 + if (get_message(cur->item, &msg))
633 + return error(_("Cannot get commit message for %s"), sha1_abbrev);
634 + strbuf_addf(buf, "%s %s %s\n", action_str, sha1_abbrev, msg.subject);
635 + }
636 + return 0;
637 +}
638 +
639 +static void walk_revs_populate_todo(struct commit_list **todo_list,
640 + struct replay_opts *opts)
641 {
642 struct rev_info revs;
643 struct commit *commit;
644 + struct commit_list **next;
645 +
646 + prepare_revs(&revs, opts);
647 +
648 + next = todo_list;
649 + while ((commit = get_revision(&revs)))
650 + next = commit_list_append(commit, next);
651 +}
652 +
653 +static void create_seq_dir(void)
654 +{
655 + const char *seq_dir = git_path(SEQ_DIR);
656 +
657 + if (!(file_exists(seq_dir) && is_directory(seq_dir))
658 + && mkdir(seq_dir, 0777) < 0)
659 + die_errno(_("Could not create sequencer directory '%s'."), seq_dir);
660 +}
661 +
662 +static void save_head(const char *head)
663 +{
664 + const char *head_file = git_path(SEQ_HEAD_FILE);
665 + static struct lock_file head_lock;
666 + struct strbuf buf = STRBUF_INIT;
667 + int fd;
668 +
669 + fd = hold_lock_file_for_update(&head_lock, head_file, LOCK_DIE_ON_ERROR);
670 + strbuf_addf(&buf, "%s\n", head);
671 + if (write_in_full(fd, buf.buf, buf.len) < 0)
672 + die_errno(_("Could not write to %s."), head_file);
673 + if (commit_lock_file(&head_lock) < 0)
674 + die(_("Error wrapping up %s."), head_file);
675 +}
676 +
677 +static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
678 +{
679 + const char *todo_file = git_path(SEQ_TODO_FILE);
680 + static struct lock_file todo_lock;
681 + struct strbuf buf = STRBUF_INIT;
682 + int fd;
683 +
684 + fd = hold_lock_file_for_update(&todo_lock, todo_file, LOCK_DIE_ON_ERROR);
685 + if (format_todo(&buf, todo_list, opts) < 0)
686 + die(_("Could not format %s."), todo_file);
687 + if (write_in_full(fd, buf.buf, buf.len) < 0) {
688 + strbuf_release(&buf);
689 + die_errno(_("Could not write to %s."), todo_file);
690 + }
691 + if (commit_lock_file(&todo_lock) < 0) {
692 + strbuf_release(&buf);
693 + die(_("Error wrapping up %s."), todo_file);
694 + }
695 + strbuf_release(&buf);
696 +}
697 +
698 +static int pick_commits(struct replay_opts *opts)
699 +{
700 + struct commit_list *todo_list = NULL;
701 + struct strbuf buf = STRBUF_INIT;
702 + unsigned char sha1[20];
703 + struct commit_list *cur;
704 + int res;
705
706 setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
707 if (opts->allow_ff)
@@ -598,14 +709,29 @@ static int pick_commits(struct replay_opts *opts)
709 opts->record_origin || opts->edit));
710 read_and_refresh_cache(opts);
711
601 - prepare_revs(&revs, opts);
712 + walk_revs_populate_todo(&todo_list, opts);
713 + create_seq_dir();
714 + if (get_sha1("HEAD", sha1)) {
715 + if (opts->action == REVERT)
716 + die(_("Can't revert as initial commit"));
717 + die(_("Can't cherry-pick into empty head"));
718 + }
719 + save_head(sha1_to_hex(sha1));
720
603 - while ((commit = get_revision(&revs))) {
604 - int res = do_pick_commit(commit, opts);
721 + for (cur = todo_list; cur; cur = cur->next) {
722 + save_todo(cur, opts);
723 + res = do_pick_commit(cur->item, opts);
724 if (res)
725 return res;
726 }
727
728 + /*
729 + * Sequence of picks finished successfully; cleanup by
730 + * removing the .git/sequencer directory
731 + */
732 + strbuf_addf(&buf, "%s", git_path(SEQ_DIR));
733 + remove_dir_recursively(&buf, 0);
734 + strbuf_release(&buf);
735 return 0;
736 }
737
t/t3510-cherry-pick-sequence.sh new
+48
@@ -0,0 +1,48 @@
1 +#!/bin/sh
2 +
3 +test_description='Test cherry-pick continuation features
4 +
5 + + anotherpick: rewrites foo to d
6 + + picked: rewrites foo to c
7 + + unrelatedpick: rewrites unrelated to reallyunrelated
8 + + base: rewrites foo to b
9 + + initial: writes foo as a, unrelated as unrelated
10 +
11 +'
12 +
13 +. ./test-lib.sh
14 +
15 +pristine_detach () {
16 + rm -rf .git/sequencer &&
17 + git checkout -f "$1^0" &&
18 + git read-tree -u --reset HEAD &&
19 + git clean -d -f -f -q -x
20 +}
21 +
22 +test_expect_success setup '
23 + echo unrelated >unrelated &&
24 + git add unrelated &&
25 + test_commit initial foo a &&
26 + test_commit base foo b &&
27 + test_commit unrelatedpick unrelated reallyunrelated &&
28 + test_commit picked foo c &&
29 + test_commit anotherpick foo d &&
30 + git config advice.detachedhead false
31 +
32 +'
33 +
34 +test_expect_success 'cherry-pick persists data on failure' '
35 + pristine_detach initial &&
36 + test_must_fail git cherry-pick base..anotherpick &&
37 + test_path_is_dir .git/sequencer &&
38 + test_path_is_file .git/sequencer/head &&
39 + test_path_is_file .git/sequencer/todo
40 +'
41 +
42 +test_expect_success 'cherry-pick cleans up sequencer state upon success' '
43 + pristine_detach initial &&
44 + git cherry-pick initial..picked &&
45 + test_path_is_missing .git/sequencer
46 +'
47 +
48 +test_done