Merge branch 'jn/maint-sequencer-fixes'

* jn/maint-sequencer-fixes:
  revert: stop creating and removing sequencer-old directory
  Revert "reset: Make reset remove the sequencer state"
  revert: do not remove state until sequence is finished
  revert: allow single-pick in the middle of cherry-pick sequence
  revert: pass around rev-list args in already-parsed form
  revert: allow cherry-pick --continue to commit before resuming
  revert: give --continue handling its own function
This commit is contained in:
Junio C Hamano 2011-12-19 16:05:45 -08:00
commit b8fc5abd73
6 changed files with 251 additions and 125 deletions

View File

@ -3,7 +3,6 @@
#include "refs.h"
#include "remote.h"
#include "commit.h"
#include "sequencer.h"
struct tracking {
struct refspec spec;
@ -280,5 +279,4 @@ void remove_branch_state(void)
unlink(git_path("MERGE_MSG"));
unlink(git_path("MERGE_MODE"));
unlink(git_path("SQUASH_MSG"));
remove_sequencer_state(0);
}

View File

@ -60,13 +60,14 @@ struct replay_opts {
int allow_rerere_auto;
int mainline;
int commit_argc;
const char **commit_argv;
/* Merge strategy */
const char *strategy;
const char **xopts;
size_t xopts_nr, xopts_alloc;
/* Only used by REPLAY_NONE */
struct rev_info *revs;
};
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@ -169,9 +170,9 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
die(_("program error"));
}
opts->commit_argc = parse_options(argc, argv, NULL, options, usage_str,
PARSE_OPT_KEEP_ARGV0 |
PARSE_OPT_KEEP_UNKNOWN);
argc = parse_options(argc, argv, NULL, options, usage_str,
PARSE_OPT_KEEP_ARGV0 |
PARSE_OPT_KEEP_UNKNOWN);
/* Check for incompatible subcommands */
verify_opt_mutually_compatible(me,
@ -213,9 +214,6 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
NULL);
}
else if (opts->commit_argc < 2)
usage_with_options(usage_str, options);
if (opts->allow_ff)
verify_opt_compatible(me, "--ff",
"--signoff", opts->signoff,
@ -223,7 +221,20 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
"-x", opts->record_origin,
"--edit", opts->edit,
NULL);
opts->commit_argv = argv;
if (opts->subcommand != REPLAY_NONE) {
opts->revs = NULL;
} else {
opts->revs = xmalloc(sizeof(*opts->revs));
init_revisions(opts->revs, NULL);
opts->revs->no_walk = 1;
if (argc < 2)
usage_with_options(usage_str, options);
argc = setup_revisions(argc, argv, opts->revs, NULL);
}
if (argc > 1)
usage_with_options(usage_str, options);
}
struct commit_message {
@ -631,23 +642,15 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
return res;
}
static void prepare_revs(struct rev_info *revs, struct replay_opts *opts)
static void prepare_revs(struct replay_opts *opts)
{
int argc;
init_revisions(revs, NULL);
revs->no_walk = 1;
if (opts->action != REVERT)
revs->reverse = 1;
opts->revs->reverse ^= 1;
argc = setup_revisions(opts->commit_argc, opts->commit_argv, revs, NULL);
if (argc > 1)
usage(*revert_or_cherry_pick_usage(opts));
if (prepare_revision_walk(revs))
if (prepare_revision_walk(opts->revs))
die(_("revision walk setup failed"));
if (!revs->commits)
if (!opts->revs->commits)
die(_("empty commit set passed"));
}
@ -844,14 +847,13 @@ static void read_populate_opts(struct replay_opts **opts_ptr)
static void walk_revs_populate_todo(struct commit_list **todo_list,
struct replay_opts *opts)
{
struct rev_info revs;
struct commit *commit;
struct commit_list **next;
prepare_revs(&revs, opts);
prepare_revs(opts);
next = todo_list;
while ((commit = get_revision(&revs)))
while ((commit = get_revision(opts->revs)))
next = commit_list_append(commit, next);
}
@ -942,7 +944,7 @@ static int sequencer_rollback(struct replay_opts *opts)
}
if (reset_for_rollback(sha1))
goto fail;
remove_sequencer_state(1);
remove_sequencer_state();
strbuf_release(&buf);
return 0;
fail:
@ -1016,33 +1018,64 @@ static int pick_commits(struct commit_list *todo_list, struct replay_opts *opts)
for (cur = todo_list; cur; cur = cur->next) {
save_todo(cur, opts);
res = do_pick_commit(cur->item, opts);
if (res) {
if (!cur->next)
/*
* An error was encountered while
* picking the last commit; the
* sequencer state is useless now --
* the user simply needs to resolve
* the conflict and commit
*/
remove_sequencer_state(0);
if (res)
return res;
}
}
/*
* Sequence of picks finished successfully; cleanup by
* removing the .git/sequencer directory
*/
remove_sequencer_state(1);
remove_sequencer_state();
return 0;
}
static int continue_single_pick(void)
{
const char *argv[] = { "commit", NULL };
if (!file_exists(git_path("CHERRY_PICK_HEAD")) &&
!file_exists(git_path("REVERT_HEAD")))
return error(_("no cherry-pick or revert in progress"));
return run_command_v_opt(argv, RUN_GIT_CMD);
}
static int sequencer_continue(struct replay_opts *opts)
{
struct commit_list *todo_list = NULL;
if (!file_exists(git_path(SEQ_TODO_FILE)))
return continue_single_pick();
read_populate_opts(&opts);
read_populate_todo(&todo_list, opts);
/* 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;
}
if (index_differs_from("HEAD", 0))
return error_dirty_index(opts);
todo_list = todo_list->next;
return pick_commits(todo_list, opts);
}
static int single_pick(struct commit *cmit, struct replay_opts *opts)
{
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
return do_pick_commit(cmit, opts);
}
static int pick_revisions(struct replay_opts *opts)
{
struct commit_list *todo_list = NULL;
unsigned char sha1[20];
if (opts->subcommand == REPLAY_NONE)
assert(opts->revs);
read_and_refresh_cache(opts);
/*
@ -1051,21 +1084,32 @@ static int pick_revisions(struct replay_opts *opts)
* one that is being continued
*/
if (opts->subcommand == REPLAY_REMOVE_STATE) {
remove_sequencer_state(1);
remove_sequencer_state();
return 0;
}
if (opts->subcommand == REPLAY_ROLLBACK)
return sequencer_rollback(opts);
if (opts->subcommand == REPLAY_CONTINUE) {
if (!file_exists(git_path(SEQ_TODO_FILE)))
return error(_("No %s in progress"), action_name(opts));
read_populate_opts(&opts);
read_populate_todo(&todo_list, opts);
if (opts->subcommand == REPLAY_CONTINUE)
return sequencer_continue(opts);
/* Verify that the conflict has been resolved */
if (!index_differs_from("HEAD", 0))
todo_list = todo_list->next;
return pick_commits(todo_list, opts);
/*
* If we were called as "git cherry-pick <commit>", just
* cherry-pick/revert it, set CHERRY_PICK_HEAD /
* REVERT_HEAD, and don't touch the sequencer state.
* This means it is possible to cherry-pick in the middle
* of a cherry-pick sequence.
*/
if (opts->revs->cmdline.nr == 1 &&
opts->revs->cmdline.rev->whence == REV_CMD_REV &&
opts->revs->no_walk &&
!opts->revs->cmdline.rev->flags) {
struct commit *cmit;
if (prepare_revision_walk(opts->revs))
die(_("revision walk setup failed"));
cmit = get_revision(opts->revs);
if (!cmit || get_revision(opts->revs))
die("BUG: expected exactly one commit from walk");
return single_pick(cmit, opts);
}
/*

View File

@ -3,17 +3,11 @@
#include "strbuf.h"
#include "dir.h"
void remove_sequencer_state(int aggressive)
void remove_sequencer_state(void)
{
struct strbuf seq_dir = STRBUF_INIT;
struct strbuf seq_old_dir = STRBUF_INIT;
strbuf_addf(&seq_dir, "%s", git_path(SEQ_DIR));
strbuf_addf(&seq_old_dir, "%s", git_path(SEQ_OLD_DIR));
remove_dir_recursively(&seq_old_dir, 0);
rename(git_path(SEQ_DIR), git_path(SEQ_OLD_DIR));
if (aggressive)
remove_dir_recursively(&seq_old_dir, 0);
remove_dir_recursively(&seq_dir, 0);
strbuf_release(&seq_dir);
strbuf_release(&seq_old_dir);
}

View File

@ -2,19 +2,11 @@
#define SEQUENCER_H
#define SEQ_DIR "sequencer"
#define SEQ_OLD_DIR "sequencer-old"
#define SEQ_HEAD_FILE "sequencer/head"
#define SEQ_TODO_FILE "sequencer/todo"
#define SEQ_OPTS_FILE "sequencer/opts"
/*
* Removes SEQ_OLD_DIR and renames SEQ_DIR to SEQ_OLD_DIR, ignoring
* any errors. Intended to be used by 'git reset'.
*
* With the aggressive flag, it additionally removes SEQ_OLD_DIR,
* ignoring any errors. Inteded to be used by the sequencer's
* '--quit' subcommand.
*/
void remove_sequencer_state(int aggressive);
/* Removes SEQ_DIR. */
extern void remove_sequencer_state(void);
#endif

View File

@ -2,6 +2,7 @@
test_description='Test cherry-pick continuation features
+ conflicting: rewrites unrelated to conflicting
+ yetanotherpick: rewrites foo to e
+ anotherpick: rewrites foo to d
+ picked: rewrites foo to c
@ -27,6 +28,7 @@ test_cmp_rev () {
}
test_expect_success setup '
git config advice.detachedhead false
echo unrelated >unrelated &&
git add unrelated &&
test_commit initial foo a &&
@ -35,8 +37,8 @@ test_expect_success setup '
test_commit picked foo c &&
test_commit anotherpick foo d &&
test_commit yetanotherpick foo e &&
git config advice.detachedhead false
pristine_detach initial &&
test_commit conflicting unrelated
'
test_expect_success 'cherry-pick persists data on failure' '
@ -48,6 +50,18 @@ test_expect_success 'cherry-pick persists data on failure' '
test_path_is_file .git/sequencer/opts
'
test_expect_success 'cherry-pick mid-cherry-pick-sequence' '
pristine_detach initial &&
test_must_fail git cherry-pick base..anotherpick &&
test_cmp_rev picked CHERRY_PICK_HEAD &&
# "oops, I forgot that these patches rely on the change from base"
git checkout HEAD foo &&
git cherry-pick base &&
git cherry-pick picked &&
git cherry-pick --continue &&
git diff --exit-code anotherpick
'
test_expect_success 'cherry-pick persists opts correctly' '
pristine_detach initial &&
test_must_fail git cherry-pick -s -m 1 --strategy=recursive -X patience -X ours base..anotherpick &&
@ -189,10 +203,10 @@ test_expect_success '--abort refuses to clobber unrelated change, harder case' '
test_cmp_rev initial HEAD
'
test_expect_success 'cherry-pick cleans up sequencer state when one commit is left' '
test_expect_success 'cherry-pick still writes sequencer state when one commit is left' '
pristine_detach initial &&
test_must_fail git cherry-pick base..picked &&
test_path_is_missing .git/sequencer &&
test_path_is_dir .git/sequencer &&
echo "resolved" >foo &&
git add foo &&
git commit &&
@ -213,7 +227,7 @@ test_expect_success 'cherry-pick cleans up sequencer state when one commit is le
test_cmp expect actual
'
test_expect_failure '--abort after last commit in sequence' '
test_expect_success '--abort after last commit in sequence' '
pristine_detach initial &&
test_must_fail git cherry-pick base..picked &&
git cherry-pick --abort &&
@ -243,7 +257,66 @@ test_expect_success '--continue complains when there are unresolved conflicts' '
test_must_fail git cherry-pick --continue
'
test_expect_success '--continue continues after conflicts are resolved' '
test_expect_success '--continue of single cherry-pick' '
pristine_detach initial &&
echo c >expect &&
test_must_fail git cherry-pick picked &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
test_cmp expect foo &&
test_cmp_rev initial HEAD^ &&
git diff --exit-code HEAD &&
test_must_fail git rev-parse --verify CHERRY_PICK_HEAD
'
test_expect_success '--continue of single revert' '
pristine_detach initial &&
echo resolved >expect &&
echo "Revert \"picked\"" >expect.msg &&
test_must_fail git revert picked &&
echo resolved >foo &&
git add foo &&
git cherry-pick --continue &&
git diff --exit-code HEAD &&
test_cmp expect foo &&
test_cmp_rev initial HEAD^ &&
git diff-tree -s --pretty=tformat:%s HEAD >msg &&
test_cmp expect.msg msg &&
test_must_fail git rev-parse --verify CHERRY_PICK_HEAD &&
test_must_fail git rev-parse --verify REVERT_HEAD
'
test_expect_success '--continue after resolving conflicts' '
pristine_detach initial &&
echo d >expect &&
cat >expect.log <<-\EOF &&
OBJID
:100644 100644 OBJID OBJID M foo
OBJID
:100644 100644 OBJID OBJID M foo
OBJID
:100644 100644 OBJID OBJID M unrelated
OBJID
:000000 100644 OBJID OBJID A foo
:000000 100644 OBJID OBJID A unrelated
EOF
test_must_fail git cherry-pick base..anotherpick &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
{
git rev-list HEAD |
git diff-tree --root --stdin |
sed "s/$_x40/OBJID/g"
} >actual.log &&
test_cmp expect foo &&
test_cmp expect.log actual.log
'
test_expect_success '--continue after resolving conflicts and committing' '
pristine_detach initial &&
test_must_fail git cherry-pick base..anotherpick &&
echo "c" >foo &&
@ -270,6 +343,29 @@ test_expect_success '--continue continues after conflicts are resolved' '
test_cmp expect actual
'
test_expect_success '--continue asks for help after resolving patch to nil' '
pristine_detach conflicting &&
test_must_fail git cherry-pick initial..picked &&
test_cmp_rev unrelatedpick CHERRY_PICK_HEAD &&
git checkout HEAD -- unrelated &&
test_must_fail git cherry-pick --continue 2>msg &&
test_i18ngrep "The previous cherry-pick is now empty" msg
'
test_expect_success 'follow advice and skip nil patch' '
pristine_detach conflicting &&
test_must_fail git cherry-pick initial..picked &&
git checkout HEAD -- unrelated &&
test_must_fail git cherry-pick --continue &&
git reset &&
git cherry-pick --continue &&
git rev-list initial..HEAD >commits &&
test_line_count = 3 commits
'
test_expect_success '--continue respects opts' '
pristine_detach initial &&
test_must_fail git cherry-pick -x base..anotherpick &&
@ -288,6 +384,29 @@ test_expect_success '--continue respects opts' '
grep "cherry picked from" anotherpick_msg
'
test_expect_success '--continue of single-pick respects -x' '
pristine_detach initial &&
test_must_fail git cherry-pick -x picked &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
test_path_is_missing .git/sequencer &&
git cat-file commit HEAD >msg &&
grep "cherry picked from" msg
'
test_expect_success '--continue respects -x in first commit in multi-pick' '
pristine_detach initial &&
test_must_fail git cherry-pick -x picked anotherpick &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
test_path_is_missing .git/sequencer &&
git cat-file commit HEAD^ >msg &&
picked=$(git rev-parse --verify picked) &&
grep "cherry picked from.*$picked" msg
'
test_expect_success '--signoff is not automatically propagated to resolved conflict' '
pristine_detach initial &&
test_must_fail git cherry-pick --signoff base..anotherpick &&
@ -306,6 +425,32 @@ test_expect_success '--signoff is not automatically propagated to resolved confl
grep "Signed-off-by:" anotherpick_msg
'
test_expect_success '--signoff dropped for implicit commit of resolution, multi-pick case' '
pristine_detach initial &&
test_must_fail git cherry-pick -s picked anotherpick &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
git diff --exit-code HEAD &&
test_cmp_rev initial HEAD^^ &&
git cat-file commit HEAD^ >msg &&
! grep Signed-off-by: msg
'
test_expect_success 'sign-off needs to be reaffirmed after conflict resolution, single-pick case' '
pristine_detach initial &&
test_must_fail git cherry-pick -s picked &&
echo c >foo &&
git add foo &&
git cherry-pick --continue &&
git diff --exit-code HEAD &&
test_cmp_rev initial HEAD^ &&
git cat-file commit HEAD >msg &&
! grep Signed-off-by: msg
'
test_expect_success 'malformed instruction sheet 1' '
pristine_detach initial &&
test_must_fail git cherry-pick base..anotherpick &&
@ -328,4 +473,9 @@ test_expect_success 'malformed instruction sheet 2' '
test_must_fail git cherry-pick --continue
'
test_expect_success 'empty commit set' '
pristine_detach initial &&
test_expect_code 128 git cherry-pick base..base
'
test_done

View File

@ -1,52 +0,0 @@
#!/bin/sh
test_description='Test interaction of reset --hard with sequencer
+ anotherpick: rewrites foo to d
+ picked: rewrites foo to c
+ unrelatedpick: rewrites unrelated to reallyunrelated
+ base: rewrites foo to b
+ initial: writes foo as a, unrelated as unrelated
'
. ./test-lib.sh
pristine_detach () {
git cherry-pick --quit &&
git checkout -f "$1^0" &&
git read-tree -u --reset HEAD &&
git clean -d -f -f -q -x
}
test_expect_success setup '
echo unrelated >unrelated &&
git add unrelated &&
test_commit initial foo a &&
test_commit base foo b &&
test_commit unrelatedpick unrelated reallyunrelated &&
test_commit picked foo c &&
test_commit anotherpick foo d &&
git config advice.detachedhead false
'
test_expect_success 'reset --hard cleans up sequencer state, providing one-level undo' '
pristine_detach initial &&
test_must_fail git cherry-pick base..anotherpick &&
test_path_is_dir .git/sequencer &&
git reset --hard &&
test_path_is_missing .git/sequencer &&
test_path_is_dir .git/sequencer-old &&
git reset --hard &&
test_path_is_missing .git/sequencer-old
'
test_expect_success 'cherry-pick --abort does not leave sequencer-old dir' '
pristine_detach initial &&
test_must_fail git cherry-pick base..anotherpick &&
git cherry-pick --abort &&
test_path_is_missing .git/sequencer &&
test_path_is_missing .git/sequencer-old
'
test_done