diff --git a/.gitignore b/.gitignore index 3677f67a1a8992..9f88b94d9c53cf 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ /git-read-tree /git-rebase /git-rebase--am +/git-rebase--helper /git-rebase--interactive /git-rebase--merge /git-receive-pack diff --git a/Makefile b/Makefile index 25a81aa2c93736..0f0a1ea47a24d1 100644 --- a/Makefile +++ b/Makefile @@ -919,6 +919,7 @@ BUILTIN_OBJS += builtin/prune.o BUILTIN_OBJS += builtin/pull.o BUILTIN_OBJS += builtin/push.o BUILTIN_OBJS += builtin/read-tree.o +BUILTIN_OBJS += builtin/rebase--helper.o BUILTIN_OBJS += builtin/receive-pack.o BUILTIN_OBJS += builtin/reflog.o BUILTIN_OBJS += builtin/remote.o diff --git a/builtin.h b/builtin.h index 6b95006a0a38d8..2e5de141ebca23 100644 --- a/builtin.h +++ b/builtin.h @@ -102,6 +102,7 @@ extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); extern int cmd_pull(int argc, const char **argv, const char *prefix); extern int cmd_push(int argc, const char **argv, const char *prefix); extern int cmd_read_tree(int argc, const char **argv, const char *prefix); +extern int cmd_rebase__helper(int argc, const char **argv, const char *prefix); extern int cmd_receive_pack(int argc, const char **argv, const char *prefix); extern int cmd_reflog(int argc, const char **argv, const char *prefix); extern int cmd_remote(int argc, const char **argv, const char *prefix); diff --git a/builtin/commit.c b/builtin/commit.c index 798482046f0215..aa200bbbb4afdf 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -183,7 +183,7 @@ static void determine_whence(struct wt_status *s) whence = FROM_MERGE; else if (file_exists(git_path_cherry_pick_head())) { whence = FROM_CHERRY_PICK; - if (file_exists(git_path(SEQ_DIR))) + if (file_exists(git_path_seq_dir())) sequencer_in_use = 1; } else diff --git a/builtin/pull.c b/builtin/pull.c index 398aae16c006f5..843ff1976d5654 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -17,6 +17,7 @@ #include "revision.h" #include "tempfile.h" #include "lockfile.h" +#include "wt-status.h" enum rebase_type { REBASE_INVALID = -1, @@ -325,73 +326,6 @@ static int git_pull_config(const char *var, const char *value, void *cb) return git_default_config(var, value, cb); } -/** - * Returns 1 if there are unstaged changes, 0 otherwise. - */ -static int has_unstaged_changes(const char *prefix) -{ - struct rev_info rev_info; - int result; - - init_revisions(&rev_info, prefix); - DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES); - DIFF_OPT_SET(&rev_info.diffopt, QUICK); - diff_setup_done(&rev_info.diffopt); - result = run_diff_files(&rev_info, 0); - return diff_result_code(&rev_info.diffopt, result); -} - -/** - * Returns 1 if there are uncommitted changes, 0 otherwise. - */ -static int has_uncommitted_changes(const char *prefix) -{ - struct rev_info rev_info; - int result; - - if (is_cache_unborn()) - return 0; - - init_revisions(&rev_info, prefix); - DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES); - DIFF_OPT_SET(&rev_info.diffopt, QUICK); - add_head_to_pending(&rev_info); - diff_setup_done(&rev_info.diffopt); - result = run_diff_index(&rev_info, 1); - return diff_result_code(&rev_info.diffopt, result); -} - -/** - * If the work tree has unstaged or uncommitted changes, dies with the - * appropriate message. - */ -static void die_on_unclean_work_tree(const char *prefix) -{ - struct lock_file *lock_file = xcalloc(1, sizeof(*lock_file)); - int do_die = 0; - - hold_locked_index(lock_file, 0); - refresh_cache(REFRESH_QUIET); - update_index_if_able(&the_index, lock_file); - rollback_lock_file(lock_file); - - if (has_unstaged_changes(prefix)) { - error(_("Cannot pull with rebase: You have unstaged changes.")); - do_die = 1; - } - - if (has_uncommitted_changes(prefix)) { - if (do_die) - error(_("Additionally, your index contains uncommitted changes.")); - else - error(_("Cannot pull with rebase: Your index contains uncommitted changes.")); - do_die = 1; - } - - if (do_die) - exit(1); -} - /** * Appends merge candidates from FETCH_HEAD that are not marked not-for-merge * into merge_heads. @@ -875,7 +809,8 @@ int cmd_pull(int argc, const char **argv, const char *prefix) die(_("Updating an unborn branch with changes added to the index.")); if (!autostash) - die_on_unclean_work_tree(prefix); + require_clean_work_tree("pull with rebase", + "Please commit or stash them.", 1, 0); if (get_rebase_fork_point(rebase_fork_point, repo, *refspecs)) hashclr(rebase_fork_point); diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c new file mode 100644 index 00000000000000..e6591f01112977 --- /dev/null +++ b/builtin/rebase--helper.c @@ -0,0 +1,67 @@ +#include "builtin.h" +#include "cache.h" +#include "parse-options.h" +#include "sequencer.h" + +static const char * const builtin_rebase_helper_usage[] = { + N_("git rebase--helper []"), + NULL +}; + +int cmd_rebase__helper(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + int keep_empty = 0; + enum { + CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S, + CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH + } command = 0; + struct option options[] = { + OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), + OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")), + OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), + CONTINUE), + OPT_CMDMODE(0, "abort", &command, N_("abort rebase"), + ABORT), + OPT_CMDMODE(0, "make-script", &command, + N_("make rebase script"), MAKE_SCRIPT), + OPT_CMDMODE(0, "shorten-sha1s", &command, + N_("shorten SHA-1s in the todo list"), SHORTEN_SHA1S), + OPT_CMDMODE(0, "expand-sha1s", &command, + N_("expand SHA-1s in the todo list"), EXPAND_SHA1S), + OPT_CMDMODE(0, "check-todo-list", &command, + N_("check the todo list"), CHECK_TODO_LIST), + OPT_CMDMODE(0, "skip-unnecessary-picks", &command, + N_("skip unnecessary picks"), SKIP_UNNECESSARY_PICKS), + OPT_CMDMODE(0, "rearrange-squash", &command, + N_("rearrange fixup/squash lines"), REARRANGE_SQUASH), + OPT_END() + }; + + git_config(git_default_config, NULL); + + opts.action = REPLAY_INTERACTIVE_REBASE; + opts.allow_ff = 1; + opts.allow_empty = 1; + + argc = parse_options(argc, argv, NULL, options, + builtin_rebase_helper_usage, PARSE_OPT_KEEP_ARGV0); + + if (command == CONTINUE && argc == 1) + return !!sequencer_continue(&opts); + if (command == ABORT && argc == 1) + return !!sequencer_remove_state(&opts); + if (command == MAKE_SCRIPT && argc > 1) + return !!sequencer_make_script(keep_empty, stdout, argc, argv); + if (command == SHORTEN_SHA1S && argc == 1) + return !!transform_todo_ids(1); + if (command == EXPAND_SHA1S && argc == 1) + return !!transform_todo_ids(0); + if (command == CHECK_TODO_LIST && argc == 1) + return !!check_todo_list(); + if (command == SKIP_UNNECESSARY_PICKS && argc == 1) + return !!skip_unnecessary_picks(); + if (command == REARRANGE_SQUASH && argc == 1) + return !!rearrange_squash(); + usage_with_options(builtin_rebase_helper_usage, options); +} diff --git a/builtin/revert.c b/builtin/revert.c index 4e693808b19778..c9ae4dc13a56a8 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -71,7 +71,7 @@ static void verify_opt_compatible(const char *me, const char *base_opt, ...) die(_("%s: %s cannot be used with %s"), me, this_opt, base_opt); } -static void parse_args(int argc, const char **argv, struct replay_opts *opts) +static int run_sequencer(int argc, const char **argv, struct replay_opts *opts) { const char * const * usage_str = revert_or_cherry_pick_usage(opts); const char *me = action_name(opts); @@ -115,25 +115,15 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts) if (opts->keep_redundant_commits) opts->allow_empty = 1; - /* Set the subcommand */ - if (cmd == 'q') - opts->subcommand = REPLAY_REMOVE_STATE; - else if (cmd == 'c') - opts->subcommand = REPLAY_CONTINUE; - else if (cmd == 'a') - opts->subcommand = REPLAY_ROLLBACK; - else - opts->subcommand = REPLAY_NONE; - /* Check for incompatible command line arguments */ - if (opts->subcommand != REPLAY_NONE) { + if (cmd) { char *this_operation; - if (opts->subcommand == REPLAY_REMOVE_STATE) + if (cmd == 'q') this_operation = "--quit"; - else if (opts->subcommand == REPLAY_CONTINUE) + else if (cmd == 'c') this_operation = "--continue"; else { - assert(opts->subcommand == REPLAY_ROLLBACK); + assert(cmd == 'a'); this_operation = "--abort"; } @@ -156,7 +146,7 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts) "--edit", opts->edit, NULL); - if (opts->subcommand != REPLAY_NONE) { + if (cmd) { opts->revs = NULL; } else { struct setup_revision_opt s_r_opt; @@ -174,20 +164,26 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts) if (argc > 1) usage_with_options(usage_str, options); + + if (cmd == 'q') + return sequencer_remove_state(opts); + if (cmd == 'c') + return sequencer_continue(opts); + if (cmd == 'a') + return sequencer_rollback(opts); + return sequencer_pick_revisions(opts); } int cmd_revert(int argc, const char **argv, const char *prefix) { - struct replay_opts opts; + struct replay_opts opts = REPLAY_OPTS_INIT; int res; - memset(&opts, 0, sizeof(opts)); if (isatty(0)) opts.edit = 1; opts.action = REPLAY_REVERT; git_config(git_default_config, NULL); - parse_args(argc, argv, &opts); - res = sequencer_pick_revisions(&opts); + res = run_sequencer(argc, argv, &opts); if (res < 0) die(_("revert failed")); return res; @@ -195,14 +191,12 @@ int cmd_revert(int argc, const char **argv, const char *prefix) int cmd_cherry_pick(int argc, const char **argv, const char *prefix) { - struct replay_opts opts; + struct replay_opts opts = REPLAY_OPTS_INIT; int res; - memset(&opts, 0, sizeof(opts)); opts.action = REPLAY_PICK; git_config(git_default_config, NULL); - parse_args(argc, argv, &opts); - res = sequencer_pick_revisions(&opts); + res = run_sequencer(argc, argv, &opts); if (res < 0) die(_("cherry-pick failed")); return res; diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index b1ba21ca2076d2..5ddac8359081ea 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -146,13 +146,13 @@ reschedule_last_action () { append_todo_help () { gettext " Commands: - p, pick = use commit - r, reword = use commit, but edit the commit message - e, edit = use commit, but stop for amending - s, squash = use commit, but meld into previous commit - f, fixup = like \"squash\", but discard this commit's log message - x, exec = run command (the rest of the line) using shell - d, drop = remove commit +p, pick = use commit +r, reword = use commit, but edit the commit message +e, edit = use commit, but stop for amending +s, squash = use commit, but meld into previous commit +f, fixup = like \"squash\", but discard this commit's log message +x, exec = run command (the rest of the line) using shell +d, drop = remove commit These lines can be re-ordered; they are executed from top to bottom. " | git stripspace --comment-lines >>"$todo" @@ -703,43 +703,6 @@ do_rest () { done } -# skip picking commits whose parents are unchanged -skip_unnecessary_picks () { - fd=3 - while read -r command rest - do - # fd=3 means we skip the command - case "$fd,$command" in - 3,pick|3,p) - # pick a commit whose parent is current $onto -> skip - sha1=${rest%% *} - case "$(git rev-parse --verify --quiet "$sha1"^)" in - "$onto"*) - onto=$sha1 - ;; - *) - fd=1 - ;; - esac - ;; - 3,"$comment_char"*|3,) - # copy comments - ;; - *) - fd=1 - ;; - esac - printf '%s\n' "$command${rest:+ }$rest" >&$fd - done <"$todo" >"$todo.new" 3>>"$done" && - mv -f "$todo".new "$todo" && - case "$(peek_next_command)" in - squash|s|fixup|f) - record_in_rewritten "$onto" - ;; - esac || - die "$(gettext "Could not skip unnecessary pick commands")" -} - transform_todo_ids () { while read -r command rest do @@ -750,7 +713,12 @@ transform_todo_ids () { ;; *) sha1=$(git rev-parse --verify --quiet "$@" ${rest%%[ ]*}) && - rest="$sha1 ${rest#*[ ]}" + if test "a$rest" = "a${rest#*[ ]}" + then + rest=$sha1 + else + rest="$sha1 ${rest#*[ ]}" + fi ;; esac printf '%s\n' "$command${rest:+ }$rest" @@ -759,98 +727,11 @@ transform_todo_ids () { } expand_todo_ids() { - transform_todo_ids + git rebase--helper --expand-sha1s } collapse_todo_ids() { - transform_todo_ids --short -} - -# Rearrange the todo list that has both "pick sha1 msg" and -# "pick sha1 fixup!/squash! msg" appears in it so that the latter -# comes immediately after the former, and change "pick" to -# "fixup"/"squash". -# -# Note that if the config has specified a custom instruction format -# each log message will be re-retrieved in order to normalize the -# autosquash arrangement -rearrange_squash () { - # extract fixup!/squash! lines and resolve any referenced sha1's - while read -r pick sha1 message - do - test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1}) - case "$message" in - "squash! "*|"fixup! "*) - action="${message%%!*}" - rest=$message - prefix= - # skip all squash! or fixup! (but save for later) - while : - do - case "$rest" in - "squash! "*|"fixup! "*) - prefix="$prefix${rest%%!*}," - rest="${rest#*! }" - ;; - *) - break - ;; - esac - done - printf '%s %s %s %s\n' "$sha1" "$action" "$prefix" "$rest" - # if it's a single word, try to resolve to a full sha1 and - # emit a second copy. This allows us to match on both message - # and on sha1 prefix - if test "${rest#* }" = "$rest"; then - fullsha="$(git rev-parse -q --verify "$rest" 2>/dev/null)" - if test -n "$fullsha"; then - # prefix the action to uniquely identify this line as - # intended for full sha1 match - echo "$sha1 +$action $prefix $fullsha" - fi - fi - esac - done >"$1.sq" <"$1" - test -s "$1.sq" || return - - used= - while read -r pick sha1 message - do - case " $used" in - *" $sha1 "*) continue ;; - esac - printf '%s\n' "$pick $sha1 $message" - test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1}) - used="$used$sha1 " - while read -r squash action msg_prefix msg_content - do - case " $used" in - *" $squash "*) continue ;; - esac - emit=0 - case "$action" in - +*) - action="${action#+}" - # full sha1 prefix test - case "$msg_content" in "$sha1"*) emit=1;; esac ;; - *) - # message prefix test - case "$message" in "$msg_content"*) emit=1;; esac ;; - esac - if test $emit = 1; then - if test -n "${format}" - then - msg_content=$(git log -n 1 --format="${format}" ${squash}) - else - msg_content="$(echo "$msg_prefix" | sed "s/,/! /g")$msg_content" - fi - printf '%s\n' "$action $squash $msg_content" - used="$used$squash " - fi - done <"$1.sq" - done >"$1.rearranged" <"$1" - cat "$1.rearranged" >"$1" - rm -f "$1.sq" "$1.rearranged" + git rebase--helper --shorten-sha1s } # Add commands after a pick or after a squash/fixup serie @@ -874,96 +755,6 @@ add_exec_commands () { mv "$1.new" "$1" } -# Check if the SHA-1 passed as an argument is a -# correct one, if not then print $2 in "$todo".badsha -# $1: the SHA-1 to test -# $2: the line number of the input -# $3: the input filename -check_commit_sha () { - badsha=0 - if test -z "$1" - then - badsha=1 - else - sha1_verif="$(git rev-parse --verify --quiet $1^{commit})" - if test -z "$sha1_verif" - then - badsha=1 - fi - fi - - if test $badsha -ne 0 - then - line="$(sed -n -e "${2}p" "$3")" - warn "$(eval_gettext "\ -Warning: the SHA-1 is missing or isn't a commit in the following line: - - \$line")" - warn - fi - - return $badsha -} - -# prints the bad commits and bad commands -# from the todolist in stdin -check_bad_cmd_and_sha () { - retval=0 - lineno=0 - while read -r command rest - do - lineno=$(( $lineno + 1 )) - case $command in - "$comment_char"*|''|noop|x|exec) - # Doesn't expect a SHA-1 - ;; - "$cr") - # Work around CR left by "read" (e.g. with Git for - # Windows' Bash). - ;; - pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f) - if ! check_commit_sha "${rest%%[ ]*}" "$lineno" "$1" - then - retval=1 - fi - ;; - *) - line="$(sed -n -e "${lineno}p" "$1")" - warn "$(eval_gettext "\ -Warning: the command isn't recognized in the following line: - - \$line")" - warn - retval=1 - ;; - esac - done <"$1" - return $retval -} - -# Print the list of the SHA-1 of the commits -# from stdin to stdout -todo_list_to_sha_list () { - git stripspace --strip-comments | - while read -r command sha1 rest - do - case $command in - "$comment_char"*|''|noop|x|"exec") - ;; - *) - long_sha=$(git rev-list --no-walk "$sha1" 2>/dev/null) - printf "%s\n" "$long_sha" - ;; - esac - done -} - -# Use warn for each line in stdin -warn_lines () { - while read -r line - do - warn " - $line" - done -} - # Switch to the branch in $into and notify it in the reflog checkout_onto () { GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name" @@ -978,74 +769,6 @@ get_missing_commit_check_level () { printf '%s' "$check_level" | tr 'A-Z' 'a-z' } -# Check if the user dropped some commits by mistake -# Behaviour determined by rebase.missingCommitsCheck. -# Check if there is an unrecognized command or a -# bad SHA-1 in a command. -check_todo_list () { - raise_error=f - - check_level=$(get_missing_commit_check_level) - - case "$check_level" in - warn|error) - # Get the SHA-1 of the commits - todo_list_to_sha_list <"$todo".backup >"$todo".oldsha1 - todo_list_to_sha_list <"$todo" >"$todo".newsha1 - - # Sort the SHA-1 and compare them - sort -u "$todo".oldsha1 >"$todo".oldsha1+ - mv "$todo".oldsha1+ "$todo".oldsha1 - sort -u "$todo".newsha1 >"$todo".newsha1+ - mv "$todo".newsha1+ "$todo".newsha1 - comm -2 -3 "$todo".oldsha1 "$todo".newsha1 >"$todo".miss - - # Warn about missing commits - if test -s "$todo".miss - then - test "$check_level" = error && raise_error=t - - warn "$(gettext "\ -Warning: some commits may have been dropped accidentally. -Dropped commits (newer to older):")" - - # Make the list user-friendly and display - opt="--no-walk=sorted --format=oneline --abbrev-commit --stdin" - git rev-list $opt <"$todo".miss | warn_lines - - warn "$(gettext "\ -To avoid this message, use \"drop\" to explicitly remove a commit. - -Use 'git config rebase.missingCommitsCheck' to change the level of warnings. -The possible behaviours are: ignore, warn, error.")" - warn - fi - ;; - ignore) - ;; - *) - warn "$(eval_gettext "Unrecognized setting \$check_level for option rebase.missingCommitsCheck. Ignoring.")" - ;; - esac - - if ! check_bad_cmd_and_sha "$todo" - then - raise_error=t - fi - - if test $raise_error = t - then - # Checkout before the first commit of the - # rebase: this way git rebase --continue - # will work correctly as it expects HEAD to be - # placed before the commit of the next action - checkout_onto - - warn "$(gettext "You can fix this with 'git rebase --edit-todo'.")" - die "$(gettext "Or you can abort the rebase with 'git rebase --abort'.")" - fi -} - # The whole contents of this file is run by dot-sourcing it from # inside a shell function. It used to be that "return"s we see # below were not inside any function, and expected to return @@ -1059,6 +782,10 @@ git_rebase__interactive () { case "$action" in continue) + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi # do we have anything to commit? if git diff-index --cached --quiet HEAD -- then @@ -1118,6 +845,10 @@ first and then run 'git rebase --continue' again.")" skip) git rerere clear + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi do_rest return 0 ;; @@ -1192,26 +923,27 @@ else revisions=$onto...$orig_head shortrevisions=$shorthead fi -format=$(git config --get rebase.instructionFormat) -# the 'rev-list .. | sed' requires %m to parse; the instruction requires %H to parse -git rev-list $merges_option --format="%m%H ${format:-%s}" \ - --reverse --left-right --topo-order \ - $revisions ${restrict_revision+^$restrict_revision} | \ - sed -n "s/^>//p" | -while read -r sha1 rest -do - - if test -z "$keep_empty" && is_empty_commit $sha1 && ! is_merge_commit $sha1 - then - comment_out="$comment_char " - else - comment_out= - fi +if test t != "$preserve_merges" +then + git rebase--helper --make-script ${keep_empty:+--keep-empty} \ + $revisions ${restrict_revision+^$restrict_revision} >"$todo" +else + format=$(git config --get rebase.instructionFormat) + # the 'rev-list .. | sed' requires %m to parse; the instruction requires %H to parse + git rev-list $merges_option --format="%m%H ${format:-%s}" \ + --reverse --left-right --topo-order \ + $revisions ${restrict_revision+^$restrict_revision} | \ + sed -n "s/^>//p" | + while read -r sha1 rest + do + + if test -z "$keep_empty" && is_empty_commit $sha1 && ! is_merge_commit $sha1 + then + comment_out="$comment_char " + else + comment_out= + fi - if test t != "$preserve_merges" - then - printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" - else if test -z "$rebase_root" then preserve=t @@ -1230,8 +962,8 @@ do touch "$rewritten"/$sha1 printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" fi - fi -done + done +fi # Watch for commits that been dropped by --cherry-pick if test t = "$preserve_merges" @@ -1261,7 +993,7 @@ then fi test -s "$todo" || echo noop >> "$todo" -test -n "$autosquash" && rearrange_squash "$todo" +test -z "$autosquash" || git rebase--helper --rearrange-squash || exit test -n "$cmd" && add_exec_commands "$todo" todocount=$(git stripspace --strip-comments <"$todo" | wc -l) @@ -1297,13 +1029,24 @@ git_sequence_editor "$todo" || has_action "$todo" || return 2 -check_todo_list +git rebase--helper --check-todo-list || { + ret=$? + checkout_onto + exit $ret +} expand_todo_ids -test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks +test -d "$rewritten" || test -n "$force_rebase" || +onto="$(git rebase--helper --skip-unnecessary-picks)" || +die "Could not skip unnecessary pick commands" checkout_onto +if test -z "$rebase_root" && test ! -d "$rewritten" +then + require_clean_work_tree "rebase" + exec git rebase--helper ${force_rebase:+--no-ff} --continue +fi do_rest } diff --git a/git.c b/git.c index 1c611517585318..7520b2421cf471 100644 --- a/git.c +++ b/git.c @@ -451,6 +451,7 @@ static struct cmd_struct commands[] = { { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE }, { "push", cmd_push, RUN_SETUP }, { "read-tree", cmd_read_tree, RUN_SETUP }, + { "rebase--helper", cmd_rebase__helper, RUN_SETUP | NEED_WORK_TREE }, { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, diff --git a/merge.c b/merge.c index 5db7d56b90c341..23866c91655638 100644 --- a/merge.c +++ b/merge.c @@ -57,7 +57,8 @@ int checkout_fast_forward(const unsigned char *head, refresh_cache(REFRESH_QUIET); - hold_locked_index(lock_file, 1); + if (hold_locked_index(lock_file, 0) < 0) + return -1; memset(&trees, 0, sizeof(trees)); memset(&opts, 0, sizeof(opts)); @@ -90,7 +91,9 @@ int checkout_fast_forward(const unsigned char *head, } if (unpack_trees(nr_trees, t, &opts)) return -1; - if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) - die(_("unable to write new index file")); + if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) { + rollback_lock_file(lock_file); + return error(_("unable to write new index file")); + } return 0; } diff --git a/run-command.c b/run-command.c index 5a4dbb66d7e4d4..921e43c4ea5359 100644 --- a/run-command.c +++ b/run-command.c @@ -575,6 +575,29 @@ int run_command_v_opt_cd_env(const char **argv, int opt, const char *dir, const cmd.clean_on_exit = opt & RUN_CLEAN_ON_EXIT ? 1 : 0; cmd.dir = dir; cmd.env = env; + + if (opt & RUN_HIDE_STDERR_ON_SUCCESS) { + struct strbuf buf = STRBUF_INIT; + int res; + + cmd.err = -1; + if (start_command(&cmd) < 0) + return -1; + + if (strbuf_read(&buf, cmd.err, 0) < 0) { + close(cmd.err); + finish_command(&cmd); /* throw away exit code */ + return -1; + } + + close(cmd.err); + res = finish_command(&cmd); + if (res) + fputs(buf.buf, stderr); + strbuf_release(&buf); + return res; + } + return run_command(&cmd); } diff --git a/run-command.h b/run-command.h index 50666497aeca8f..f87d01a0d61186 100644 --- a/run-command.h +++ b/run-command.h @@ -70,6 +70,7 @@ extern int run_hook_ve(const char *const *env, const char *name, va_list args); #define RUN_SILENT_EXEC_FAILURE 8 #define RUN_USING_SHELL 16 #define RUN_CLEAN_ON_EXIT 32 +#define RUN_HIDE_STDERR_ON_SUCCESS 64 int run_command_v_opt(const char **argv, int opt); /* diff --git a/sequencer.c b/sequencer.c index 3804fa931d819f..4b50a4c9b7fa86 100644 --- a/sequencer.c +++ b/sequencer.c @@ -15,16 +15,136 @@ #include "merge-recursive.h" #include "refs.h" #include "argv-array.h" +#include "quote.h" +#include "log-tree.h" +#include "wt-status.h" +#include "hashmap.h" #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" const char sign_off_header[] = "Signed-off-by: "; static const char cherry_picked_prefix[] = "(cherry picked from commit "; -static GIT_PATH_FUNC(git_path_todo_file, SEQ_TODO_FILE) -static GIT_PATH_FUNC(git_path_opts_file, SEQ_OPTS_FILE) -static GIT_PATH_FUNC(git_path_seq_dir, SEQ_DIR) -static GIT_PATH_FUNC(git_path_head_file, SEQ_HEAD_FILE) +GIT_PATH_FUNC(git_path_seq_dir, "sequencer") + +static GIT_PATH_FUNC(git_path_todo_file, "sequencer/todo") +static GIT_PATH_FUNC(git_path_opts_file, "sequencer/opts") +static GIT_PATH_FUNC(git_path_head_file, "sequencer/head") + +static GIT_PATH_FUNC(rebase_path, "rebase-merge") +/* + * The file containing rebase commands, comments, and empty lines. + * This file is created by "git rebase -i" then edited by the user. As + * the lines are processed, they are removed from the front of this + * file and written to the tail of 'done'. + */ +static GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") +/* + * The rebase command lines that have already been processed. A line + * is moved here when it is first handled, before any associated user + * actions. + */ +static GIT_PATH_FUNC(rebase_path_done, "rebase-merge/done") +/* + * The file to keep track of how many commands were already processed (e.g. + * for the prompt). + */ +static GIT_PATH_FUNC(rebase_path_msgnum, "rebase-merge/msgnum"); +/* + * The file to keep track of how many commands are to be processed in total + * (e.g. for the prompt). + */ +static GIT_PATH_FUNC(rebase_path_msgtotal, "rebase-merge/end"); +/* + * The commit message that is planned to be used for any changes that + * need to be committed following a user interaction. + */ +static GIT_PATH_FUNC(rebase_path_message, "rebase-merge/message") +/* + * The file into which is accumulated the suggested commit message for + * squash/fixup commands. When the first of a series of squash/fixups + * is seen, the file is created and the commit message from the + * previous commit and from the first squash/fixup commit are written + * to it. The commit message for each subsequent squash/fixup commit + * is appended to the file as it is processed. + * + * The first line of the file is of the form + * # This is a combination of $count commits. + * where $count is the number of commits whose messages have been + * written to the file so far (including the initial "pick" commit). + * Each time that a commit message is processed, this line is read and + * updated. It is deleted just before the combined commit is made. + */ +static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash") +/* + * If the current series of squash/fixups has not yet included a squash + * command, then this file exists and holds the commit message of the + * original "pick" commit. (If the series ends without a "squash" + * command, then this can be used as the commit message of the combined + * commit without opening the editor.) + */ +static GIT_PATH_FUNC(rebase_path_fixup_msg, "rebase-merge/message-fixup") +/* + * A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and + * GIT_AUTHOR_DATE that will be used for the commit that is currently + * being rebased. + */ +static GIT_PATH_FUNC(rebase_path_author_script, "rebase-merge/author-script") +/* + * When an "edit" rebase command is being processed, the SHA1 of the + * commit to be edited is recorded in this file. When "git rebase + * --continue" is executed, if there are any staged changes then they + * will be amended to the HEAD commit, but only provided the HEAD + * commit is still the commit to be edited. When any other rebase + * command is processed, this file is deleted. + */ +static GIT_PATH_FUNC(rebase_path_amend, "rebase-merge/amend") +/* + * When we stop at a given patch via the "edit" command, this file contains + * the long commit name of the corresponding patch. + */ +static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha") +/* + * For the post-rewrite hook, we make a list of rewritten commits and + * their new sha1s. The rewritten-pending list keeps the sha1s of + * commits that have been processed, but not committed yet, + * e.g. because they are waiting for a 'squash' command. + */ +static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list") +static GIT_PATH_FUNC(rebase_path_rewritten_pending, + "rebase-merge/rewritten-pending") +/* + * The following files are written by git-rebase just after parsing the + * command-line (and are only consumed, not modified, by the sequencer). + */ +static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt") +static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head") +static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose") +static GIT_PATH_FUNC(rebase_path_head_name, "rebase-merge/head-name") +static GIT_PATH_FUNC(rebase_path_onto, "rebase-merge/onto") +static GIT_PATH_FUNC(rebase_path_autostash, "rebase-merge/autostash") +static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy") +static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts") + +/* We will introduce the 'interactive rebase' mode later */ +static inline int is_rebase_i(const struct replay_opts *opts) +{ + return opts->action == REPLAY_INTERACTIVE_REBASE; +} + +static const char *get_dir(const struct replay_opts *opts) +{ + if (is_rebase_i(opts)) + return rebase_path(); + return git_path_seq_dir(); +} + +static const char *get_todo_path(const struct replay_opts *opts) +{ + if (is_rebase_i(opts)) + return rebase_path_todo(); + return git_path_todo_file(); +} static int is_rfc2822_line(const char *buf, int len) { @@ -108,18 +228,53 @@ static int has_conforming_footer(struct strbuf *sb, struct strbuf *sob, return 1; } -static void remove_sequencer_state(void) +static const char *gpg_sign_opt_quoted(struct replay_opts *opts) +{ + static struct strbuf buf = STRBUF_INIT; + + strbuf_reset(&buf); + if (opts->gpg_sign) + sq_quotef(&buf, "-S%s", opts->gpg_sign); + return buf.buf; +} + +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++] = to_free; + + return to_free; +} + +int sequencer_remove_state(struct replay_opts *opts) { - struct strbuf seq_dir = STRBUF_INIT; + struct strbuf dir = STRBUF_INIT; + int i; - strbuf_addstr(&seq_dir, git_path(SEQ_DIR)); - remove_dir_recursively(&seq_dir, 0); - strbuf_release(&seq_dir); + for (i = 0; i < opts->owned_nr; i++) + free(opts->owned[i]); + free(opts->owned); + + free(opts->xopts); + + strbuf_addf(&dir, "%s", get_dir(opts)); + remove_dir_recursively(&dir, 0); + strbuf_release(&dir); + + return 0; } static const char *action_name(const struct replay_opts *opts) { - return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; + switch (opts->action) { + case REPLAY_REVERT: + return "revert"; + case REPLAY_PICK: + return "cherry-pick"; + case REPLAY_INTERACTIVE_REBASE: + return "rebase -i"; + } + die("Unknown action: %d", opts->action); } struct commit_message { @@ -129,13 +284,18 @@ struct commit_message { const char *message; }; +static const char *short_commit_name(struct commit *commit) +{ + return find_unique_abbrev(commit->object.oid.hash, DEFAULT_ABBREV); +} + static int get_message(struct commit *commit, struct commit_message *out) { const char *abbrev, *subject; int subject_len; out->message = logmsg_reencode(commit, NULL, get_commit_output_encoding()); - abbrev = find_unique_abbrev(commit->object.oid.hash, DEFAULT_ABBREV); + abbrev = short_commit_name(commit); subject_len = find_commit_subject(out->message, &subject); @@ -180,17 +340,66 @@ static void print_advice(int show_hint, struct replay_opts *opts) } } -static void write_message(struct strbuf *msgbuf, const char *filename) +static int write_with_lock_file(const char *filename, + const void *buf, size_t len, int append_eol) { static struct lock_file msg_file; - int msg_fd = hold_lock_file_for_update(&msg_file, filename, - LOCK_DIE_ON_ERROR); - if (write_in_full(msg_fd, msgbuf->buf, msgbuf->len) < 0) - die_errno(_("Could not write to %s"), filename); - strbuf_release(msgbuf); + int msg_fd = hold_lock_file_for_update(&msg_file, filename, 0); + 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); + if (append_eol && write(msg_fd, "\n", 1) < 0) + return error_errno(_("Could not write eol to '%s"), filename); if (commit_lock_file(&msg_file) < 0) - die(_("Error wrapping up %s."), filename); + return error(_("Error wrapping up %s."), filename); + + return 0; +} + +static int write_message(struct strbuf *msgbuf, const char *filename) +{ + int res = write_with_lock_file(filename, msgbuf->buf, msgbuf->len, 0); + strbuf_release(msgbuf); + return res; +} + +static int write_file_gently(const char *filename, + const char *text, int append_eol) +{ + return write_with_lock_file(filename, text, strlen(text), append_eol); +} + +/* + * Reads a file that was presumably written by a shell script, i.e. + * with an end-of-line marker that needs to be stripped. + * + * Returns 1 if the file was read, 0 if it could not be read or does not exist. + */ +static int read_oneliner(struct strbuf *buf, + const char *path, int skip_if_empty) +{ + int orig_len = buf->len; + + if (!file_exists(path)) + return 0; + + if (strbuf_read_file(buf, path, 0) < 0) { + warning_errno("could not read '%s'", path); + return 0; + } + + if (buf->len > orig_len && buf->buf[buf->len - 1] == '\n') { + if (--buf->len > orig_len && buf->buf[buf->len - 1] == '\r') + --buf->len; + buf->buf[buf->len] = '\0'; + } + + if (skip_if_empty && buf->len == orig_len) + return 0; + + return 1; } static struct tree *empty_tree(void) @@ -203,11 +412,8 @@ static int error_dirty_index(struct replay_opts *opts) if (read_cache_unmerged()) return error_resolve_conflict(action_name(opts)); - /* Different translation strings for cherry-pick and revert */ - if (opts->action == REPLAY_PICK) - error(_("Your local changes would be overwritten by cherry-pick.")); - else - error(_("Your local changes would be overwritten by revert.")); + error(_("Your local changes would be overwritten by %s."), + action_name(opts)); if (advice_commit_before_merge) advise(_("Commit your changes or stash them to proceed.")); @@ -223,7 +429,7 @@ static int fast_forward_to(const unsigned char *to, const unsigned char *from, read_cache(); if (checkout_fast_forward(from, to, 1)) - exit(128); /* the callee should have complained already */ + return -1; /* the callee should have complained already */ strbuf_addf(&sb, _("%s: fast-forward"), action_name(opts)); @@ -282,6 +488,8 @@ static int do_recursive_merge(struct commit *base, struct commit *next, o.ancestor = base ? base_label : "(empty tree)"; o.branch1 = "HEAD"; o.branch2 = next ? next_label : "(empty tree)"; + if (is_rebase_i(opts)) + o.buffer_output = 2; head_tree = parse_tree_indirect(head); next_tree = next ? next->tree : empty_tree(); @@ -293,14 +501,20 @@ static int do_recursive_merge(struct commit *base, struct commit *next, clean = merge_trees(&o, head_tree, next_tree, base_tree, &result); + if (is_rebase_i(opts) && clean <= 0) + fputs(o.obuf.buf, stdout); strbuf_release(&o.obuf); if (clean < 0) return clean; if (active_cache_changed && write_locked_index(&the_index, &index_lock, COMMIT_LOCK)) - /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ - die(_("%s: Unable to write new index file"), action_name(opts)); + /* + * TRANSLATORS: %s will be "revert", "cherry-pick" or + * "rebase -i". + */ + return error(_("%s: Unable to write new index file"), + action_name(opts)); rollback_lock_file(&index_lock); if (opts->signoff) @@ -343,36 +557,150 @@ static int is_index_unchanged(void) return !hashcmp(active_cache_tree->sha1, head_commit->tree->object.oid.hash); } +static int write_author_script(const char *message) +{ + struct strbuf buf = STRBUF_INIT; + const char *eol; + + for (;;) + if (!*message || starts_with(message, "\n")) { +missing_author: + /* Missing 'author' line? */ + unlink(rebase_path_author_script()); + return 0; + } + else if (skip_prefix(message, "author ", &message)) + break; + else if ((eol = strchr(message, '\n'))) + message = eol + 1; + else + goto missing_author; + + strbuf_addstr(&buf, "GIT_AUTHOR_NAME='"); + while (*message && *message != '\n' && *message != '\r') + if (skip_prefix(message, " <", &message)) + break; + else if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + strbuf_addstr(&buf, "'\nGIT_AUTHOR_EMAIL='"); + while (*message && *message != '\n' && *message != '\r') + if (skip_prefix(message, "> ", &message)) + break; + else if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + strbuf_addstr(&buf, "'\nGIT_AUTHOR_DATE='@"); + while (*message && *message != '\n' && *message != '\r') + if (*message != '\'') + strbuf_addch(&buf, *(message++)); + else + strbuf_addf(&buf, "'\\\\%c'", *(message++)); + strbuf_addstr(&buf, "'\n"); + return write_message(&buf, rebase_path_author_script()); +} + +static char **read_author_script(void) +{ + struct strbuf script = STRBUF_INIT; + int i, count = 0; + char *p, *p2, **env; + size_t env_size; + + if (strbuf_read_file(&script, rebase_path_author_script(), 256) <= 0) + return NULL; + + for (p = script.buf; *p; p++) + if (skip_prefix(p, "'\\\\''", (const char **)&p2)) + strbuf_splice(&script, p - script.buf, p2 - p, "'", 1); + else if (*p == '\'') + strbuf_splice(&script, p-- - script.buf, 1, "", 0); + else if (*p == '\n') { + *p = '\0'; + count++; + } + + env_size = (count + 1) * sizeof(*env); + strbuf_grow(&script, env_size); + memmove(script.buf + env_size, script.buf, script.len); + p = script.buf + env_size; + env = (char **)strbuf_detach(&script, NULL); + + for (i = 0; i < count; i++) { + env[i] = p; + p += strlen(p) + 1; + } + env[count] = NULL; + + return env; +} + /* * 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. + * + * An exception is when sequencer_commit() is called during an + * interactive rebase: in that case, we will want to retain the + * author metadata. */ -static int run_git_commit(const char *defmsg, struct replay_opts *opts, - int allow_empty) +int sequencer_commit(const char *defmsg, struct replay_opts *opts, + int allow_empty, int edit, int amend, + int cleanup_commit_message) { + char **env = NULL; struct argv_array array; - int rc; + int opt = RUN_GIT_CMD, rc; const char *value; + if (is_rebase_i(opts)) { + if (!edit) { + opt |= RUN_COMMAND_STDOUT_TO_STDERR; + opt |= RUN_HIDE_STDERR_ON_SUCCESS; + } + + env = read_author_script(); + if (!env) { + const char *gpg_opt = gpg_sign_opt_quoted(opts); + + return error("You have staged changes in your working " + "tree. If these changes are meant to be\n" + "squashed into the previous commit, run:\n\n" + " git commit --amend %s\n\n" + "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 " + "with:\n\n" + " git rebase --continue\n", gpg_opt, gpg_opt); + } + } + argv_array_init(&array); argv_array_push(&array, "commit"); argv_array_push(&array, "-n"); + if (amend) + argv_array_push(&array, "--amend"); if (opts->gpg_sign) argv_array_pushf(&array, "-S%s", opts->gpg_sign); if (opts->signoff) argv_array_push(&array, "-s"); - if (!opts->edit) { - argv_array_push(&array, "-F"); - argv_array_push(&array, defmsg); - if (!opts->signoff && - !opts->record_origin && - git_config_get_value("commit.cleanup", &value)) - argv_array_push(&array, "--cleanup=verbatim"); - } + if (defmsg) + argv_array_pushl(&array, "-F", defmsg, NULL); + if (cleanup_commit_message) + argv_array_push(&array, "--cleanup=strip"); + if (edit) + argv_array_push(&array, "-e"); + else if (!cleanup_commit_message && + !opts->signoff && !opts->record_origin && + git_config_get_value("commit.cleanup", &value)) + argv_array_push(&array, "--cleanup=verbatim"); if (allow_empty) argv_array_push(&array, "--allow-empty"); @@ -380,8 +708,11 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, if (opts->allow_empty_message) argv_array_push(&array, "--allow-empty-message"); - rc = run_command_v_opt(array.argv, RUN_GIT_CMD); + rc = run_command_v_opt_cd_env(array.argv, opt, NULL, + (const char *const *)env); argv_array_clear(&array); + free(env); + return rc; } @@ -443,14 +774,198 @@ static int allow_empty(struct replay_opts *opts, struct commit *commit) return 1; } -static int do_pick_commit(struct commit *commit, struct replay_opts *opts) +/* + * Note that ordering matters in this enum. Not only must it match the mapping + * below, it is also divided into several sections that matter. When adding + * new commands, make sure you add it in the right section. + */ +enum todo_command { + /* commands that handle commits */ + TODO_PICK = 0, + TODO_REVERT, + TODO_EDIT, + TODO_REWORD, + TODO_FIXUP, + TODO_SQUASH, + /* commands that do something else than handling a single commit */ + TODO_EXEC, + /* commands that do nothing but are counted for reporting progress */ + TODO_NOOP, + TODO_DROP, + /* comments (not counted for reporting progress) */ + TODO_COMMENT +}; + +static struct { + char c; + const char *str; +} todo_command_info[] = { + { 'p', "pick" }, + { 0, "revert" }, + { 'e', "edit" }, + { 'r', "reword" }, + { 'f', "fixup" }, + { 's', "squash" }, + { 'x', "exec" }, + { 0, "noop" }, + { 'd', "drop" }, + { 0, NULL } +}; + +static const char *command_to_string(const enum todo_command command) +{ + if (command < TODO_COMMENT) + return todo_command_info[command].str; + die("Unknown command: %d", command); +} + +static int is_fixup(enum todo_command command) +{ + return command == TODO_FIXUP || command == TODO_SQUASH; +} + +static int update_squash_messages(enum todo_command command, + struct commit *commit, struct replay_opts *opts) +{ + struct strbuf buf = STRBUF_INIT; + int count; + const char *message, *body; + + if (file_exists(rebase_path_squash_msg())) { + char *p, *p2; + + if (strbuf_read_file(&buf, rebase_path_squash_msg(), 2048) <= 0) + return error(_("Could not read '%s'"), + rebase_path_squash_msg()); + + if (buf.buf[0] == '\n' || !skip_prefix(buf.buf + 1, + " This is a combination of ", + (const char **)&p)) + return error(_("Unexpected 1st line of squash message:" + "\n\n\t%.*s"), + (int)(strchrnul(buf.buf, '\n') - buf.buf), + buf.buf); + count = strtol(p, &p2, 10); + + if (count < 1 || *p2 != ' ') + return error(_("Invalid 1st line of squash message:\n" + "\n\t%.*s"), + (int)(strchrnul(buf.buf, '\n') - buf.buf), + buf.buf); + + sprintf((char *)p, "%d", ++count); + if (!*p2) + *p2 = ' '; + else { + *(++p2) = 'c'; + strbuf_insert(&buf, p2 - buf.buf, " ", 1); + } + } + else { + unsigned char head[20]; + struct commit *head_commit; + const char *head_message, *body; + + if (get_sha1("HEAD", head)) + return error(_("Need a HEAD to fixup")); + if (!(head_commit = lookup_commit_reference(head))) + return error(_("Could not read HEAD")); + if (!(head_message = get_commit_buffer(head_commit, NULL))) + return error(_("Could not read HEAD's commit message")); + + body = strstr(head_message, "\n\n"); + if (!body) + body = ""; + else + body = skip_blank_lines(body + 2); + if (write_file_gently(rebase_path_fixup_msg(), body, 0)) + return error(_("Cannot write '%s'"), + rebase_path_fixup_msg()); + + count = 2; + strbuf_addf(&buf, _("%c This is a combination of 2 commits.\n" + "%c The first commit's message is:\n\n%s"), + comment_line_char, comment_line_char, body); + + unuse_commit_buffer(head_commit, head_message); + } + + if (!(message = get_commit_buffer(commit, NULL))) + return error(_("Could not read commit message of %s"), + oid_to_hex(&commit->object.oid)); + body = strstr(message, "\n\n"); + if (!body) + body = ""; + else + body = skip_blank_lines(body + 2); + + if (command == TODO_SQUASH) { + unlink(rebase_path_fixup_msg()); + strbuf_addf(&buf, _("\n%c This is the commit message #%d:\n" + "\n%s"), + comment_line_char, count, body); + } + else if (command == TODO_FIXUP) { + strbuf_addf(&buf, _("\n%c The commit message #%d " + "will be skipped:\n\n"), + comment_line_char, count); + strbuf_add_commented_lines(&buf, body, strlen(body)); + } + else + return error(_("Unknown command: %d"), command); + unuse_commit_buffer(commit, message); + + return write_message(&buf, rebase_path_squash_msg()); +} + +static void flush_rewritten_pending(void) { + struct strbuf buf = STRBUF_INIT; + unsigned char newsha1[20]; + FILE *out; + + if (strbuf_read_file(&buf, rebase_path_rewritten_pending(), 82) > 0 && + !get_sha1("HEAD", newsha1) && + (out = fopen(rebase_path_rewritten_list(), "a"))) { + char *bol = buf.buf, *eol; + + while (*bol) { + eol = strchrnul(bol, '\n'); + fprintf(out, "%.*s %s\n", (int)(eol - bol), + bol, sha1_to_hex(newsha1)); + if (!*eol) + break; + bol = eol + 1; + } + fclose(out); + unlink(rebase_path_rewritten_pending()); + } +} + +static void record_in_rewritten(struct object_id *oid, + enum todo_command next_command) { + FILE *out = fopen(rebase_path_rewritten_pending(), "a"); + + if (!out) + return; + + fprintf(out, "%s\n", oid_to_hex(oid)); + fclose(out); + + if (!is_fixup(next_command)) + flush_rewritten_pending(); +} + +static int do_pick_commit(enum todo_command command, struct commit *commit, + struct replay_opts *opts, int final_fixup) { + int edit = opts->edit, cleanup_commit_message = 0; + const char *msg_file = edit ? NULL : git_path_merge_msg(); unsigned char head[20]; struct commit *base, *next, *parent; const char *base_label, *next_label; struct commit_message msg = { NULL, NULL, NULL, NULL }; struct strbuf msgbuf = STRBUF_INIT; - int res, unborn = 0, allow; + int res = 0, unborn = 0, amend = 0, allow = 0; if (opts->no_commit) { /* @@ -460,7 +975,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) * to work on. */ if (write_cache_as_tree(head, 0, NULL)) - die (_("Your index file is unmerged.")); + return error(_("Your index file is unmerged.")); } else { unborn = get_sha1("HEAD", head); if (unborn) @@ -496,20 +1011,29 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) else parent = commit->parents->item; - if (opts->allow_ff && - ((parent && !hashcmp(parent->object.oid.hash, head)) || - (!parent && unborn))) - return fast_forward_to(commit->object.oid.hash, head, unborn, opts); + if (get_message(commit, &msg) != 0) + return error(_("Cannot get commit message for %s"), + oid_to_hex(&commit->object.oid)); + if (opts->allow_ff && !is_fixup(command) && + ((parent && !hashcmp(parent->object.oid.hash, head)) || + (!parent && unborn))) { + if (is_rebase_i(opts)) + write_author_script(msg.message); + res |= fast_forward_to(commit->object.oid.hash, head, unborn, + opts); + if (res || command != TODO_REWORD) + goto leave; + edit = amend = 1; + msg_file = NULL; + goto fast_forward_edit; + } 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"), - action_name(opts), oid_to_hex(&parent->object.oid)); - - if (get_message(commit, &msg) != 0) - return error(_("Cannot get commit message for %s"), - oid_to_hex(&commit->object.oid)); + command_to_string(command), + oid_to_hex(&parent->object.oid)); /* * "commit" is an existing commit. We would want to apply @@ -518,7 +1042,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) * reverse of it if we are revert. */ - if (opts->action == REPLAY_REVERT) { + if (command == TODO_REVERT) { base = commit; base_label = msg.label; next = parent; @@ -559,21 +1083,47 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) } } - if (!opts->strategy || !strcmp(opts->strategy, "recursive") || opts->action == REPLAY_REVERT) { - res = do_recursive_merge(base, next, base_label, next_label, + if (command == TODO_REWORD) + edit = 1; + else if (is_fixup(command)) { + if (update_squash_messages(command, commit, opts)) + return -1; + amend = 1; + if (!final_fixup) + msg_file = rebase_path_squash_msg(); + else if (file_exists(rebase_path_fixup_msg())) { + cleanup_commit_message = 1; + msg_file = rebase_path_fixup_msg(); + } + else { + const char *dest = git_path("SQUASH_MSG"); + unlink(dest); + if (copy_file(dest, rebase_path_squash_msg(), 0666)) + return error("Could not rename %s to " + "%s", rebase_path_squash_msg(), dest); + unlink(git_path("MERGE_MSG")); + msg_file = dest; + edit = 1; + } + } + + if (is_rebase_i(opts) && write_author_script(msg.message) < 0) + res |= -1; + else if (!opts->strategy || !strcmp(opts->strategy, "recursive") || command == TODO_REVERT) { + res |= do_recursive_merge(base, next, base_label, next_label, head, &msgbuf, opts); if (res < 0) return res; - write_message(&msgbuf, git_path_merge_msg()); + res |= write_message(&msgbuf, git_path_merge_msg()); } else { struct commit_list *common = NULL; struct commit_list *remotes = NULL; - write_message(&msgbuf, git_path_merge_msg()); + res |= write_message(&msgbuf, git_path_merge_msg()); commit_list_insert(base, &common); commit_list_insert(next, &remotes); - res = try_merge_command(opts->strategy, opts->xopts_nr, opts->xopts, + res |= try_merge_command(opts->strategy, opts->xopts_nr, opts->xopts, common, sha1_to_hex(head), remotes); free_commit_list(common); free_commit_list(remotes); @@ -585,19 +1135,20 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) * However, if the merge did not even start, then we don't want to * write it at all. */ - if (opts->action == REPLAY_PICK && !opts->no_commit && (res == 0 || res == 1)) - update_ref(NULL, "CHERRY_PICK_HEAD", commit->object.oid.hash, NULL, - REF_NODEREF, UPDATE_REFS_DIE_ON_ERR); - if (opts->action == REPLAY_REVERT && ((opts->no_commit && res == 0) || res == 1)) - update_ref(NULL, "REVERT_HEAD", commit->object.oid.hash, NULL, - REF_NODEREF, UPDATE_REFS_DIE_ON_ERR); + if (command == TODO_PICK && !opts->no_commit && (res == 0 || res == 1) && + update_ref(NULL, "CHERRY_PICK_HEAD", commit->object.oid.hash, NULL, + REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) + res = -1; + if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1) && + update_ref(NULL, "REVERT_HEAD", commit->object.oid.hash, NULL, + REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) + res = -1; if (res) { - error(opts->action == REPLAY_REVERT + error(command == TODO_REVERT ? _("could not revert %s... %s") : _("could not apply %s... %s"), - find_unique_abbrev(commit->object.oid.hash, DEFAULT_ABBREV), - msg.subject); + short_commit_name(commit), msg.subject); print_advice(res == 1, opts); rerere(opts->allow_rerere_auto); goto leave; @@ -605,11 +1156,18 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) allow = allow_empty(opts, commit); if (allow < 0) { - res = allow; + res |= allow; goto leave; } if (!opts->no_commit) - res = run_git_commit(git_path_merge_msg(), opts, allow); +fast_forward_edit: + res |= sequencer_commit(msg_file, opts, allow, edit, amend, + cleanup_commit_message); + + if (!res && final_fixup) { + unlink(rebase_path_fixup_msg()); + unlink(rebase_path_squash_msg()); + } leave: free_message(commit, &msg); @@ -617,7 +1175,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) return res; } -static void prepare_revs(struct replay_opts *opts) +static int prepare_revs(struct replay_opts *opts) { /* * picking (but not reverting) ranges (but not individual revisions) @@ -627,137 +1185,233 @@ static void prepare_revs(struct replay_opts *opts) opts->revs->reverse ^= 1; if (prepare_revision_walk(opts->revs)) - die(_("revision walk setup failed")); + return error(_("revision walk setup failed")); if (!opts->revs->commits) - die(_("empty commit set passed")); + return error(_("empty commit set passed")); + return 0; } -static void read_and_refresh_cache(struct replay_opts *opts) +static int read_and_refresh_cache(struct replay_opts *opts) { static struct lock_file index_lock; int index_fd = hold_locked_index(&index_lock, 0); - if (read_index_preload(&the_index, NULL) < 0) - die(_("git %s: failed to read the index"), action_name(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)); + } 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)) - die(_("git %s: failed to refresh the index"), action_name(opts)); + 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)); + } } rollback_lock_file(&index_lock); + return 0; } -static int format_todo(struct strbuf *buf, struct commit_list *todo_list, - struct replay_opts *opts) +struct todo_item { + enum todo_command command; + struct commit *commit; + const char *arg; + int arg_len; + size_t offset_in_buf; +}; + +struct todo_list { + struct strbuf buf; + struct todo_item *items; + int nr, alloc, current; + int done_nr, total_nr; +}; + +#define TODO_LIST_INIT { STRBUF_INIT } + +static void todo_list_release(struct todo_list *todo_list) { - struct commit_list *cur = NULL; - const char *sha1_abbrev = NULL; - const char *action_str = opts->action == REPLAY_REVERT ? "revert" : "pick"; - const char *subject; - int subject_len; + strbuf_release(&todo_list->buf); + free(todo_list->items); + todo_list->items = NULL; + todo_list->nr = todo_list->alloc = 0; +} - for (cur = todo_list; cur; cur = cur->next) { - const char *commit_buffer = get_commit_buffer(cur->item, NULL); - sha1_abbrev = find_unique_abbrev(cur->item->object.oid.hash, DEFAULT_ABBREV); - subject_len = find_commit_subject(commit_buffer, &subject); - strbuf_addf(buf, "%s %s %.*s\n", action_str, sha1_abbrev, - subject_len, subject); - unuse_commit_buffer(cur->item, commit_buffer); - } - return 0; +struct todo_item *append_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++; } -static struct commit *parse_insn_line(char *bol, char *eol, struct replay_opts *opts) +static int parse_insn_line(struct todo_item *item, const char *bol, char *eol) { unsigned char commit_sha1[20]; - enum replay_action action; char *end_of_object_name; - int saved, status, padding; - - if (starts_with(bol, "pick")) { - action = REPLAY_PICK; - bol += strlen("pick"); - } else if (starts_with(bol, "revert")) { - action = REPLAY_REVERT; - bol += strlen("revert"); - } else - return NULL; + int i, saved, status, padding; + + /* left-trim */ + bol += strspn(bol, " \t"); + + if (bol == eol || *bol == '\r' || *bol == comment_line_char) { + item->command = TODO_COMMENT; + item->commit = NULL; + item->arg = bol; + item->arg_len = eol - bol; + return 0; + } + + for (i = 0; i < TODO_COMMENT; i++) + if (skip_prefix(bol, todo_command_info[i].str, &bol)) { + item->command = i; + break; + } + else if (bol[1] == ' ' && *bol == todo_command_info[i].c) { + bol++; + item->command = i; + break; + } + if (i >= TODO_COMMENT) + return -1; + + if (item->command == TODO_NOOP) { + item->commit = NULL; + item->arg = bol; + item->arg_len = eol - bol; + return 0; + } /* Eat up extra spaces/ tabs before object name */ padding = strspn(bol, " \t"); if (!padding) - return NULL; + return -1; bol += padding; - end_of_object_name = bol + strcspn(bol, " \t\n"); + if (item->command == TODO_EXEC) { + item->arg = bol; + item->arg_len = (int)(eol - bol); + return 0; + } + + end_of_object_name = (char *) bol + strcspn(bol, " \t\n"); saved = *end_of_object_name; *end_of_object_name = '\0'; status = get_sha1(bol, commit_sha1); *end_of_object_name = saved; - /* - * Verify that the action matches up with the one in - * opts; we don't support arbitrary instructions - */ - if (action != opts->action) { - if (action == REPLAY_REVERT) - error((opts->action == REPLAY_REVERT) - ? _("Cannot revert during another revert.") - : _("Cannot revert during a cherry-pick.")); - else - error((opts->action == REPLAY_REVERT) - ? _("Cannot cherry-pick during a revert.") - : _("Cannot cherry-pick during another cherry-pick.")); - return NULL; - } + item->arg = end_of_object_name + strspn(end_of_object_name, " \t"); + item->arg_len = (int)(eol - item->arg); if (status < 0) - return NULL; + return -1; - return lookup_commit_reference(commit_sha1); + item->commit = lookup_commit_reference(commit_sha1); + return !item->commit; } -static int parse_insn_buffer(char *buf, struct commit_list **todo_list, - struct replay_opts *opts) +static int parse_insn_buffer(char *buf, struct todo_list *todo_list) { - struct commit_list **next = todo_list; - struct commit *commit; + struct todo_item *item; char *p = buf; - int i; + int i, res = 0, fixup_okay = file_exists(rebase_path_done()); for (i = 1; *p; i++) { char *eol = strchrnul(p, '\n'); - commit = parse_insn_line(p, eol, opts); - if (!commit) - return error(_("Could not parse line %d."), i); - next = commit_list_append(commit, next); + + item = append_todo(todo_list); + item->offset_in_buf = p - todo_list->buf.buf; + if (parse_insn_line(item, p, eol)) { + res |= error(_("Invalid line %d: %.*s"), + i, (int)(eol - p), p); + item->command = TODO_NOOP; + } + if (fixup_okay) + ; /* do nothing */ + else if (is_fixup(item->command)) + return error("Cannot '%s' without a previous commit", + command_to_string(item->command)); + else if (item->command < TODO_NOOP) + fixup_okay = 1; p = *eol ? eol + 1 : eol; } - if (!*todo_list) - return error(_("No commits parsed.")); - return 0; + + return res; +} + +static int count_commands(struct todo_list *todo_list) +{ + int count = 0, i; + + for (i = 0; i < todo_list->nr; i++) + if (todo_list->items[i].command != TODO_COMMENT) + count++; + + return count; } -static void read_populate_todo(struct commit_list **todo_list, +static int read_populate_todo(struct todo_list *todo_list, struct replay_opts *opts) { - struct strbuf buf = STRBUF_INIT; + const char *todo_file = get_todo_path(opts); int fd, res; - fd = open(git_path_todo_file(), O_RDONLY); + strbuf_reset(&todo_list->buf); + fd = open(todo_file, O_RDONLY); if (fd < 0) - die_errno(_("Could not open %s"), git_path_todo_file()); - if (strbuf_read(&buf, fd, 0) < 0) { + return error_errno(_("Could not open %s"), todo_file); + if (strbuf_read(&todo_list->buf, fd, 0) < 0) { close(fd); - strbuf_release(&buf); - die(_("Could not read %s."), git_path_todo_file()); + return error(_("Could not read %s."), todo_file); } close(fd); - res = parse_insn_buffer(buf.buf, todo_list, opts); - strbuf_release(&buf); - if (res) - die(_("Unusable instruction sheet: %s"), git_path_todo_file()); + res = parse_insn_buffer(todo_list->buf.buf, todo_list); + if (res) { + if (is_rebase_i(opts)) + return error("Please fix this using " + "'git rebase --edit-todo'."); + return error(_("Unusable instruction sheet: %s"), todo_file); + } + + if (!todo_list->nr && + (!is_rebase_i(opts) || !file_exists(rebase_path_done()))) + return error(_("No commits parsed.")); + + 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.")); + } + + if (is_rebase_i(opts)) { + struct todo_list done = TODO_LIST_INIT; + FILE *f = fopen(rebase_path_msgtotal(), "w"); + + if (strbuf_read_file(&done.buf, rebase_path_done(), 0) > 0 && + !parse_insn_buffer(done.buf.buf, &done)) + todo_list->done_nr = count_commands(&done); + else + todo_list->done_nr = 0; + todo_list_release(&done); + + todo_list->total_nr = todo_list->done_nr + + count_commands(todo_list); + + if (f) { + fprintf(f, "%d\n", todo_list->total_nr); + fclose(f); + } + } + + return 0; } static int populate_opts_cb(const char *key, const char *value, void *data) @@ -779,13 +1433,18 @@ static int populate_opts_cb(const char *key, const char *value, void *data) opts->allow_ff = git_config_bool_or_int(key, value, &error_flag); else if (!strcmp(key, "options.mainline")) opts->mainline = git_config_int(key, value); - else if (!strcmp(key, "options.strategy")) + else if (!strcmp(key, "options.strategy")) { git_config_string(&opts->strategy, key, value); - else if (!strcmp(key, "options.gpg-sign")) + sequencer_entrust(opts, (char *) opts->strategy); + } + else if (!strcmp(key, "options.gpg-sign")) { git_config_string(&opts->gpg_sign, key, value); + sequencer_entrust(opts, (char *) opts->gpg_sign); + } else if (!strcmp(key, "options.strategy-option")) { ALLOC_GROW(opts->xopts, opts->xopts_nr + 1, opts->xopts_alloc); - opts->xopts[opts->xopts_nr++] = xstrdup(value); + opts->xopts[opts->xopts_nr++] = + sequencer_entrust(opts, xstrdup(value)); } else return error(_("Invalid key: %s"), key); @@ -795,25 +1454,89 @@ static int populate_opts_cb(const char *key, const char *value, void *data) return 0; } -static void read_populate_opts(struct replay_opts **opts_ptr) +static int read_populate_opts(struct replay_opts *opts) { - if (!file_exists(git_path_opts_file())) - return; - if (git_config_from_file(populate_opts_cb, git_path_opts_file(), *opts_ptr) < 0) - die(_("Malformed options sheet: %s"), git_path_opts_file()); -} + if (is_rebase_i(opts)) { + struct strbuf buf = STRBUF_INIT; -static void walk_revs_populate_todo(struct commit_list **todo_list, + if (read_oneliner(&buf, rebase_path_gpg_sign_opt(), 1)) { + if (!starts_with(buf.buf, "-S")) + strbuf_reset(&buf); + else { + opts->gpg_sign = buf.buf + 2; + sequencer_entrust(opts, + strbuf_detach(&buf, NULL)); + } + } + + if (file_exists(rebase_path_verbose())) + opts->verbose = 1; + + if (read_oneliner(&buf, rebase_path_strategy(), 0)) { + opts->strategy = + sequencer_entrust(opts, + strbuf_detach(&buf, NULL)); + if (read_oneliner(&buf, + rebase_path_strategy_opts(), 0)) { + int i; + opts->xopts_nr = split_cmdline(buf.buf, + &opts->xopts); + for (i = 0; i < opts->xopts_nr; i++) + skip_prefix(opts->xopts[i], "--", + &opts->xopts[i]); + if (opts->xopts_nr) + sequencer_entrust(opts, + strbuf_detach(&buf, NULL)); + else + strbuf_release(&buf); + } + } + + return 0; + } + + if (!file_exists(git_path_opts_file())) + return 0; + /* + * The function git_parse_source(), called from git_config_from_file(), + * may die() in case of a syntactically incorrect file. We do not care + * about this case, though, because we wrote that file ourselves, so we + * 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"), + git_path_opts_file()); + return 0; +} + +static int walk_revs_populate_todo(struct todo_list *todo_list, struct replay_opts *opts) { + enum todo_command command = opts->action == REPLAY_PICK ? + TODO_PICK : TODO_REVERT; + const char *command_string = todo_command_info[command].str; struct commit *commit; - struct commit_list **next; - prepare_revs(opts); + if (prepare_revs(opts)) + return -1; - next = todo_list; - while ((commit = get_revision(opts->revs))) - next = commit_list_append(commit, next); + while ((commit = get_revision(opts->revs))) { + struct todo_item *item = append_todo(todo_list); + const char *commit_buffer = get_commit_buffer(commit, NULL); + const char *subject; + int subject_len; + + item->command = command; + item->commit = commit; + item->arg = NULL; + 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", command_string, + short_commit_name(commit), subject_len, subject); + unuse_commit_buffer(commit, commit_buffer); + } + return 0; } static int create_seq_dir(void) @@ -824,23 +1547,33 @@ static int create_seq_dir(void) return -1; } else if (mkdir(git_path_seq_dir(), 0777) < 0) - die_errno(_("Could not create sequencer directory %s"), - git_path_seq_dir()); + return error_errno(_("Could not create sequencer directory %s"), + git_path_seq_dir()); return 0; } -static void save_head(const char *head) +static int save_head(const char *head) { static struct lock_file head_lock; struct strbuf buf = STRBUF_INIT; int fd; - fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), LOCK_DIE_ON_ERROR); + fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), 0); + if (fd < 0) { + rollback_lock_file(&head_lock); + return error_errno(_("Could not lock HEAD")); + } strbuf_addf(&buf, "%s\n", head); - if (write_in_full(fd, buf.buf, buf.len) < 0) - die_errno(_("Could not write to %s"), git_path_head_file()); - if (commit_lock_file(&head_lock) < 0) - die(_("Error wrapping up %s."), git_path_head_file()); + if (write_in_full(fd, buf.buf, buf.len) < 0) { + rollback_lock_file(&head_lock); + 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 0; } static int reset_for_rollback(const unsigned char *sha1) @@ -867,7 +1600,7 @@ static int rollback_single_pick(void) return reset_for_rollback(head_sha1); } -static int sequencer_rollback(struct replay_opts *opts) +int sequencer_rollback(struct replay_opts *opts) { FILE *f; unsigned char sha1[20]; @@ -902,91 +1635,477 @@ static int sequencer_rollback(struct replay_opts *opts) } if (reset_for_rollback(sha1)) goto fail; - remove_sequencer_state(); strbuf_release(&buf); - return 0; + return sequencer_remove_state(opts); fail: strbuf_release(&buf); return -1; } -static void save_todo(struct commit_list *todo_list, struct replay_opts *opts) +static int save_todo(struct todo_list *todo_list, struct replay_opts *opts) { static struct lock_file todo_lock; - struct strbuf buf = STRBUF_INIT; - int fd; + const char *todo_path = get_todo_path(opts); + int next = todo_list->current, offset, fd; - fd = hold_lock_file_for_update(&todo_lock, git_path_todo_file(), LOCK_DIE_ON_ERROR); - if (format_todo(&buf, todo_list, opts) < 0) - die(_("Could not format %s."), git_path_todo_file()); - if (write_in_full(fd, buf.buf, buf.len) < 0) { - strbuf_release(&buf); - die_errno(_("Could not write to %s"), git_path_todo_file()); - } - if (commit_lock_file(&todo_lock) < 0) { - strbuf_release(&buf); - die(_("Error wrapping up %s."), git_path_todo_file()); + if (is_rebase_i(opts)) + next++; + + fd = hold_lock_file_for_update(&todo_lock, todo_path, 0); + if (fd < 0) + 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_errno(_("Could not write to '%s'"), todo_path); + if (commit_lock_file(&todo_lock) < 0) + return error(_("Error wrapping up %s."), todo_path); + + if (is_rebase_i(opts)) { + const char *done_path = rebase_path_done(); + int fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666); + int prev_offset = !next ? 0 : + todo_list->items[next - 1].offset_in_buf; + + if (offset > prev_offset && write_in_full(fd, + todo_list->buf.buf + prev_offset, + offset - prev_offset) < 0) + return error_errno(_("Could not write to '%s'"), + done_path); + close(fd); } - strbuf_release(&buf); + return 0; } -static void save_opts(struct replay_opts *opts) +static int save_opts(struct replay_opts *opts) { const char *opts_file = git_path_opts_file(); + int res = 0; if (opts->no_commit) - git_config_set_in_file(opts_file, "options.no-commit", "true"); + res |= git_config_set_in_file_gently(opts_file, "options.no-commit", "true"); if (opts->edit) - git_config_set_in_file(opts_file, "options.edit", "true"); + res |= git_config_set_in_file_gently(opts_file, "options.edit", "true"); if (opts->signoff) - git_config_set_in_file(opts_file, "options.signoff", "true"); + res |= git_config_set_in_file_gently(opts_file, "options.signoff", "true"); if (opts->record_origin) - git_config_set_in_file(opts_file, "options.record-origin", "true"); + res |= git_config_set_in_file_gently(opts_file, "options.record-origin", "true"); if (opts->allow_ff) - git_config_set_in_file(opts_file, "options.allow-ff", "true"); + res |= git_config_set_in_file_gently(opts_file, "options.allow-ff", "true"); if (opts->mainline) { struct strbuf buf = STRBUF_INIT; strbuf_addf(&buf, "%d", opts->mainline); - git_config_set_in_file(opts_file, "options.mainline", buf.buf); + res |= git_config_set_in_file_gently(opts_file, "options.mainline", buf.buf); strbuf_release(&buf); } if (opts->strategy) - git_config_set_in_file(opts_file, "options.strategy", opts->strategy); + res |= git_config_set_in_file_gently(opts_file, "options.strategy", opts->strategy); if (opts->gpg_sign) - git_config_set_in_file(opts_file, "options.gpg-sign", opts->gpg_sign); + res |= git_config_set_in_file_gently(opts_file, "options.gpg-sign", opts->gpg_sign); if (opts->xopts) { int i; for (i = 0; i < opts->xopts_nr; i++) - git_config_set_multivar_in_file(opts_file, + res |= git_config_set_multivar_in_file_gently(opts_file, "options.strategy-option", opts->xopts[i], "^$", 0); } + return res; } -static int pick_commits(struct commit_list *todo_list, struct replay_opts *opts) +static int make_patch(struct commit *commit, struct replay_opts *opts) { - struct commit_list *cur; - int res; + struct strbuf buf = STRBUF_INIT; + struct rev_info log_tree_opt; + const char *commit_buffer = get_commit_buffer(commit, NULL), *subject; + int res = 0; + + if (write_file_gently(rebase_path_stopped_sha(), + short_commit_name(commit), 1) < 0) + return -1; + + strbuf_addf(&buf, "%s/patch", get_dir(opts)); + memset(&log_tree_opt, 0, sizeof(log_tree_opt)); + init_revisions(&log_tree_opt, NULL); + log_tree_opt.abbrev = 0; + log_tree_opt.diff = 1; + log_tree_opt.diffopt.output_format = DIFF_FORMAT_PATCH; + log_tree_opt.disable_stdin = 1; + log_tree_opt.no_commit_id = 1; + log_tree_opt.diffopt.file = fopen(buf.buf, "w"); + log_tree_opt.diffopt.use_color = GIT_COLOR_NEVER; + if (!log_tree_opt.diffopt.file) + res |= error_errno("could not open '%s'", buf.buf); + else { + res |= log_tree_commit(&log_tree_opt, commit); + fclose(log_tree_opt.diffopt.file); + } + strbuf_reset(&buf); + + strbuf_addf(&buf, "%s/message", get_dir(opts)); + if (!file_exists(buf.buf)) { + find_commit_subject(commit_buffer, &subject); + res |= write_file_gently(buf.buf, subject, 1); + unuse_commit_buffer(commit, commit_buffer); + } + strbuf_release(&buf); + + return res; +} + +static int intend_to_amend(void) +{ + unsigned char head[20]; + + if (get_sha1("HEAD", head)) + return error("Cannot read HEAD"); + + return write_file_gently(rebase_path_amend(), sha1_to_hex(head), 1); +} + +static int error_with_patch(struct commit *commit, + const char *subject, int subject_len, + struct replay_opts *opts, int exit_code, int to_amend) +{ + if (make_patch(commit, opts)) + return -1; + + if (to_amend) { + if (intend_to_amend()) + return -1; + + fprintf(stderr, "You can amend the commit now, with\n" + "\n" + " git commit --amend %s\n" + "\n" + "Once you are satisfied with your changes, run\n" + "\n" + " git rebase --continue\n", gpg_sign_opt_quoted(opts)); + } + else if (exit_code) + fprintf(stderr, "Could not apply %s... %.*s\n", + short_commit_name(commit), subject_len, subject); + + return exit_code; +} + +static int error_failed_squash(struct commit *commit, + struct replay_opts *opts, int subject_len, const char *subject) +{ + if (rename(rebase_path_squash_msg(), rebase_path_message())) + return error("Could not rename %s to %s", + rebase_path_squash_msg(), rebase_path_message()); + unlink(rebase_path_fixup_msg()); + unlink(git_path("MERGE_MSG")); + if (copy_file(git_path("MERGE_MSG"), rebase_path_message(), 0666)) + return error("Could not copy %s to %s", rebase_path_message(), + git_path("MERGE_MSG")); + return error_with_patch(commit, subject, subject_len, opts, 1, 0); +} + +static int do_exec(const char *command_line) +{ + const char *child_argv[] = { NULL, NULL }; + int dirty, status; + + fprintf(stderr, "Executing: %s\n", command_line); + child_argv[0] = command_line; + status = run_command_v_opt(child_argv, RUN_USING_SHELL); + + /* force re-reading of the cache */ + if (discard_cache() < 0 || read_cache() < 0) + return error(_("Could not read index")); + + dirty = require_clean_work_tree("rebase", NULL, 1, 1); + + if (status) { + warning("Execution failed: %s\n%s" + "You can fix the problem, and then run\n" + "\n" + " git rebase --continue\n" + "\n", + command_line, + dirty ? "and made changes to the index and/or the " + "working tree\n" : ""); + if (status == 127) + /* command not found */ + status = 1; + } + else if (dirty) { + warning("Execution succeeded: %s\nbut " + "left changes to the index and/or the working tree\n" + "Commit or stash your changes, and then run\n" + "\n" + " git rebase --continue\n" + "\n", command_line); + status = 1; + } + + return status; +} + +static int is_final_fixup(struct todo_list *todo_list) +{ + int i = todo_list->current; + + if (!is_fixup(todo_list->items[i].command)) + return 0; + + while (++i < todo_list->nr) + if (is_fixup(todo_list->items[i].command)) + return 0; + else if (todo_list->items[i].command < TODO_NOOP) + break; + return 1; +} + +static enum todo_command peek_command(struct todo_list *todo_list, int offset) +{ + int i; + + for (i = todo_list->current + offset; i < todo_list->nr; i++) + if (todo_list->items[i].command < TODO_NOOP) + return todo_list->items[i].command; + + return -1; +} + +static int apply_autostash(struct replay_opts *opts) +{ + struct strbuf stash_sha1 = STRBUF_INIT; + struct child_process child = CHILD_PROCESS_INIT; + int ret = 0; + + if (!read_oneliner(&stash_sha1, rebase_path_autostash(), 1)) { + strbuf_release(&stash_sha1); + return 0; + } + strbuf_trim(&stash_sha1); + + child.git_cmd = 1; + argv_array_push(&child.args, "stash"); + argv_array_push(&child.args, "apply"); + argv_array_push(&child.args, stash_sha1.buf); + if (!run_command(&child)) + printf(_("Applied autostash.")); + else { + struct child_process store = CHILD_PROCESS_INIT; + + store.git_cmd = 1; + argv_array_push(&store.args, "stash"); + argv_array_push(&store.args, "store"); + argv_array_push(&store.args, "-m"); + argv_array_push(&store.args, "autostash"); + argv_array_push(&store.args, "-q"); + argv_array_push(&store.args, stash_sha1.buf); + if (run_command(&store)) + ret = error(_("Cannot store %s"), stash_sha1.buf); + else + printf(_("Applying autostash resulted in conflicts.\n" + "Your changes are safe in the stash.\n" + "You can run \"git stash pop\" or" + " \"git stash drop\" at any time.\n")); + } + + strbuf_release(&stash_sha1); + return ret; +} + +static const char *reflog_message(struct replay_opts *opts, + const char *sub_action, const char *fmt, ...) +{ + va_list ap; + static struct strbuf buf = STRBUF_INIT; + + va_start(ap, fmt); + strbuf_reset(&buf); + strbuf_addstr(&buf, action_name(opts)); + if (sub_action) + strbuf_addf(&buf, " (%s)", sub_action); + if (fmt) { + strbuf_addstr(&buf, ": "); + strbuf_vaddf(&buf, fmt, ap); + } + va_end(ap); + + return buf.buf; +} + +static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts) +{ + int res = 0; setenv(GIT_REFLOG_ACTION, action_name(opts), 0); if (opts->allow_ff) assert(!(opts->signoff || opts->no_commit || opts->record_origin || opts->edit)); - read_and_refresh_cache(opts); + if (read_and_refresh_cache(opts)) + return -1; - for (cur = todo_list; cur; cur = cur->next) { - save_todo(cur, opts); - res = do_pick_commit(cur->item, opts); + while (todo_list->current < todo_list->nr) { + struct todo_item *item = todo_list->items + todo_list->current; + if (save_todo(todo_list, opts)) + return -1; + if (is_rebase_i(opts)) { + if (item->command != TODO_COMMENT) { + FILE *f = fopen(rebase_path_msgnum(), "w"); + + todo_list->done_nr++; + + if (f) { + fprintf(f, "%d\n", todo_list->done_nr); + fclose(f); + } + fprintf(stderr, "Rebasing (%d/%d)%s", + todo_list->done_nr, + todo_list->total_nr, + opts->verbose ? "\n" : "\r"); + } + unlink(rebase_path_message()); + unlink(rebase_path_author_script()); + unlink(rebase_path_stopped_sha()); + unlink(rebase_path_amend()); + } + if (item->command <= TODO_SQUASH) { + if (is_rebase_i(opts)) + setenv("GIT_REFLOG_ACTION", reflog_message(opts, + command_to_string(item->command), NULL), + 1); + res = do_pick_commit(item->command, item->commit, + opts, is_final_fixup(todo_list)); + if (is_rebase_i(opts) && res < 0) { + /* Reschedule */ + todo_list->current--; + if (save_todo(todo_list, opts)) + return -1; + } + if (item->command == TODO_EDIT) { + struct commit *commit = item->commit; + if (!res) + warning("Stopped at %s... %.*s", + short_commit_name(commit), + item->arg_len, item->arg); + return error_with_patch(commit, + item->arg, item->arg_len, opts, res, + !res); + } + if (is_rebase_i(opts) && !res) + record_in_rewritten(&item->commit->object.oid, + peek_command(todo_list, 1)); + if (res && is_fixup(item->command)) { + if (res == 1) + intend_to_amend(); + return error_failed_squash(item->commit, opts, + item->arg_len, item->arg); + } + else if (res && is_rebase_i(opts)) + return res | error_with_patch(item->commit, + item->arg, item->arg_len, opts, res, + item->command == TODO_REWORD); + } + else if (item->command == TODO_EXEC) { + char *end_of_arg = (char *)(item->arg + item->arg_len); + int saved = *end_of_arg; + + *end_of_arg = '\0'; + res = do_exec(item->arg); + *end_of_arg = saved; + } + else if (item->command < TODO_NOOP) + return error("Unknown command %d", item->command); + + todo_list->current++; if (res) return res; } + if (is_rebase_i(opts)) { + struct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT; + struct stat st; + + /* Stopped in the middle, as planned? */ + if (todo_list->current < todo_list->nr) + return 0; + + if (read_oneliner(&head_ref, rebase_path_head_name(), 0) && + starts_with(head_ref.buf, "refs/")) { + const char *msg; + unsigned char head[20], orig[20]; + + if (get_sha1("HEAD", head)) + return error("Cannot read HEAD"); + if (!read_oneliner(&buf, rebase_path_orig_head(), 0) || + get_sha1_hex(buf.buf, orig)) + return error("Could not read orig-head"); + if (!read_oneliner(&buf, rebase_path_onto(), 0)) + return error("Could not read 'onto'"); + msg = reflog_message(opts, "finish", "%s onto %s", + head_ref.buf, buf.buf); + if (update_ref(msg, head_ref.buf, head, orig, + REF_NODEREF, UPDATE_REFS_MSG_ON_ERR)) + return error("Could not update %s", + head_ref.buf); + msg = reflog_message(opts, "finish", "returning to %s", + head_ref.buf); + if (create_symref("HEAD", head_ref.buf, msg)) + return error("Could not update HEAD to %s", + head_ref.buf); + strbuf_reset(&buf); + } + + if (opts->verbose) { + const char *argv[] = { + "diff-tree", "--stat", NULL, NULL + }; + + if (!read_oneliner(&buf, rebase_path_orig_head(), 0)) + return error("Could not read %s", + rebase_path_orig_head()); + strbuf_addstr(&buf, "..HEAD"); + argv[2] = buf.buf; + run_command_v_opt(argv, RUN_GIT_CMD); + strbuf_reset(&buf); + } + flush_rewritten_pending(); + if (!stat(rebase_path_rewritten_list(), &st) && + st.st_size > 0) { + struct child_process child = CHILD_PROCESS_INIT; + const char *post_rewrite_hook = + find_hook("post-rewrite"); + + child.in = open(rebase_path_rewritten_list(), O_RDONLY); + child.git_cmd = 1; + argv_array_push(&child.args, "notes"); + argv_array_push(&child.args, "copy"); + argv_array_push(&child.args, "--for-rewrite=rebase"); + /* we don't care if this copying failed */ + run_command(&child); + + if (post_rewrite_hook) { + struct child_process hook = CHILD_PROCESS_INIT; + + hook.in = open(rebase_path_rewritten_list(), + O_RDONLY); + argv_array_push(&hook.args, post_rewrite_hook); + argv_array_push(&hook.args, "rebase"); + /* we don't care if this hook failed */ + run_command(&hook); + } + } + apply_autostash(opts); + + fprintf(stderr, "Successfully rebased and updated %s.\n", + head_ref.buf); + + strbuf_release(&buf); + strbuf_release(&head_ref); + } + /* * Sequence of picks finished successfully; cleanup by * removing the .git/sequencer directory */ - remove_sequencer_state(); - return 0; + return sequencer_remove_state(opts); } static int continue_single_pick(void) @@ -999,58 +2118,113 @@ static int continue_single_pick(void) return run_command_v_opt(argv, RUN_GIT_CMD); } -static int sequencer_continue(struct replay_opts *opts) +static int commit_staged_changes(struct replay_opts *opts) { - struct commit_list *todo_list = NULL; + int amend = 0; + + if (has_unstaged_changes(1)) + return error(_("Cannot rebase: You have unstaged changes.")); + if (!has_uncommitted_changes(0)) { + const char *cherry_pick_head = git_path("CHERRY_PICK_HEAD"); + + if (file_exists(cherry_pick_head) && unlink(cherry_pick_head)) + return error("Could not remove CHERRY_PICK_HEAD"); + return 0; + } + + if (file_exists(rebase_path_amend())) { + struct strbuf rev = STRBUF_INIT; + unsigned char head[20], to_amend[20]; + + if (get_sha1("HEAD", head)) + return error("Cannot amend non-existing commit"); + if (!read_oneliner(&rev, rebase_path_amend(), 0)) + return error("Invalid file: %s", rebase_path_amend()); + if (get_sha1_hex(rev.buf, to_amend)) + return error("Invalid contents: %s", + rebase_path_amend()); + if (hashcmp(head, to_amend)) + return error("\nYou have uncommitted changes in your " + "working tree. Please, commit them\nfirst and " + "then run 'git rebase --continue' again."); + + strbuf_release(&rev); + amend = 1; + } + + if (sequencer_commit(rebase_path_message(), opts, 1, 1, amend, 0)) + return error("Could not commit staged changes."); + unlink(rebase_path_amend()); + return 0; +} - if (!file_exists(git_path_todo_file())) +int sequencer_continue(struct replay_opts *opts) +{ + struct todo_list todo_list = TODO_LIST_INIT; + int res; + + if (read_and_refresh_cache(opts)) + return -1; + + if (is_rebase_i(opts)) { + if (commit_staged_changes(opts)) + return -1; + } + else if (!file_exists(get_todo_path(opts))) return continue_single_pick(); - 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; + + if (!is_rebase_i(opts)) { + /* Verify that the conflict has been resolved */ + if (file_exists(git_path_cherry_pick_head()) || + file_exists(git_path_revert_head())) { + 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; + } + todo_list.current++; + } + else if (file_exists(rebase_path_stopped_sha())) { + struct strbuf buf = STRBUF_INIT; + struct object_id oid; - /* 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 (read_oneliner(&buf, rebase_path_stopped_sha(), 1)) { + if (!get_sha1_committish(buf.buf, oid.hash)) + record_in_rewritten(&oid, + peek_command(&todo_list, 0)); + } + strbuf_release(&buf); } - if (index_differs_from("HEAD", 0)) - return error_dirty_index(opts); - todo_list = todo_list->next; - return pick_commits(todo_list, opts); + + res = pick_commits(&todo_list, opts); +release_todo_list: + todo_list_release(&todo_list); + return res; } 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); + return do_pick_commit(opts->action == REPLAY_PICK ? + TODO_PICK : TODO_REVERT, cmit, opts, 0); } int sequencer_pick_revisions(struct replay_opts *opts) { - struct commit_list *todo_list = NULL; + struct todo_list todo_list = TODO_LIST_INIT; unsigned char sha1[20]; - int i; - - if (opts->subcommand == REPLAY_NONE) - assert(opts->revs); + int i, res; - read_and_refresh_cache(opts); - - /* - * Decide what to do depending on the arguments; a fresh - * cherry-pick should be handled differently from an existing - * one that is being continued - */ - if (opts->subcommand == REPLAY_REMOVE_STATE) { - remove_sequencer_state(); - return 0; - } - if (opts->subcommand == REPLAY_ROLLBACK) - return sequencer_rollback(opts); - if (opts->subcommand == REPLAY_CONTINUE) - return sequencer_continue(opts); + assert(opts->revs); + if (read_and_refresh_cache(opts)) + return -1; for (i = 0; i < opts->revs->pending.nr; i++) { unsigned char sha1[20]; @@ -1063,10 +2237,11 @@ int sequencer_pick_revisions(struct replay_opts *opts) if (!get_sha1(name, sha1)) { if (!lookup_commit_reference_gently(sha1, 1)) { enum object_type type = sha1_object_info(sha1, NULL); - die(_("%s: can't cherry-pick a %s"), name, typename(type)); + return error(_("%s: can't cherry-pick a %s"), + name, typename(type)); } } else - die(_("%s: bad revision"), name); + return error(_("%s: bad revision"), name); } /* @@ -1082,10 +2257,10 @@ int sequencer_pick_revisions(struct replay_opts *opts) !opts->revs->cmdline.rev->flags) { struct commit *cmit; if (prepare_revision_walk(opts->revs)) - die(_("revision walk setup failed")); + return error(_("revision walk setup failed")); cmit = get_revision(opts->revs); if (!cmit || get_revision(opts->revs)) - die("BUG: expected exactly one commit from walk"); + return error("BUG: expected exactly one commit from walk"); return single_pick(cmit, opts); } @@ -1095,14 +2270,18 @@ int sequencer_pick_revisions(struct replay_opts *opts) * progress */ - walk_revs_populate_todo(&todo_list, opts); - if (create_seq_dir() < 0) + if (walk_revs_populate_todo(&todo_list, opts) || + create_seq_dir() < 0) return -1; if (get_sha1("HEAD", sha1) && (opts->action == REPLAY_REVERT)) return error(_("Can't revert as initial commit")); - save_head(sha1_to_hex(sha1)); - save_opts(opts); - return pick_commits(todo_list, opts); + if (save_head(sha1_to_hex(sha1))) + return -1; + if (save_opts(opts)) + return -1; + res = pick_commits(&todo_list, opts); + todo_list_release(&todo_list); + return res; } void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag) @@ -1169,3 +2348,516 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag) strbuf_release(&sob); } + +int sequencer_make_script(int keep_empty, FILE *out, + int argc, const char **argv) +{ + char *format = "%s"; + struct pretty_print_context pp = {0}; + struct strbuf buf = STRBUF_INIT; + struct rev_info revs; + struct commit *commit; + + init_revisions(&revs, NULL); + revs.verbose_header = 1; + revs.max_parents = 1; + revs.cherry_pick = 1; + revs.limited = 1; + revs.reverse = 1; + revs.right_only = 1; + revs.sort_order = REV_SORT_IN_GRAPH_ORDER; + revs.topo_order = 1; + + revs.pretty_given = 1; + git_config_get_string("rebase.instructionFormat", &format); + get_commit_format(format, &revs); + pp.fmt = revs.commit_format; + pp.output_encoding = get_log_output_encoding(); + + if (setup_revisions(argc, argv, &revs, NULL) > 1) + return error("make_script: unhandled options"); + + if (prepare_revision_walk(&revs) < 0) + return error("make_script: error preparing revisions"); + + while ((commit = get_revision(&revs))) { + strbuf_reset(&buf); + if (!keep_empty && is_original_commit_empty(commit)) + strbuf_addf(&buf, "%c ", comment_line_char); + strbuf_addf(&buf, "pick %s ", oid_to_hex(&commit->object.oid)); + pretty_print_commit(&pp, commit, &buf); + strbuf_addch(&buf, '\n'); + fputs(buf.buf, out); + } + strbuf_release(&buf); + return 0; +} + + +int transform_todo_ids(int shorten_sha1s) +{ + const char *todo_file = rebase_path_todo(); + struct todo_list todo_list = TODO_LIST_INIT; + int fd, res, i; + FILE *out; + + strbuf_reset(&todo_list.buf); + fd = open(todo_file, O_RDONLY); + if (fd < 0) + 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); + } + close(fd); + + res = parse_insn_buffer(todo_list.buf.buf, &todo_list); + if (res) { + todo_list_release(&todo_list); + return error(_("Unusable instruction sheet: %s"), todo_file); + } + + out = fopen(todo_file, "w"); + if (!out) { + todo_list_release(&todo_list); + return error(_("Unable to open '%s' for writing"), todo_file); + } + for (i = 0; i < todo_list.nr; i++) { + struct todo_item *item = todo_list.items + i; + int bol = item->offset_in_buf; + const char *p = todo_list.buf.buf + bol; + int eol = i + 1 < todo_list.nr ? + todo_list.items[i + 1].offset_in_buf : + todo_list.buf.len; + + if (item->command >= TODO_EXEC && item->command != TODO_DROP) + fwrite(p, eol - bol, 1, out); + else { + int eoc = strcspn(p, " \t"); + const char *sha1 = shorten_sha1s ? + short_commit_name(item->commit) : + oid_to_hex(&item->commit->object.oid); + + if (!eoc) { + p += strspn(p, " \t"); + eoc = strcspn(p, " \t"); + } + + fprintf(out, "%.*s %s %.*s\n", + eoc, p, sha1, item->arg_len, item->arg); + } + } + fclose(out); + todo_list_release(&todo_list); + return 0; +} + +enum check_level { + CHECK_IGNORE = 0, CHECK_WARN, CHECK_ERROR +}; + +static enum check_level get_missing_commit_check_level(void) +{ + const char *value; + + if (git_config_get_value("rebase.missingcommitscheck", &value) || + !strcasecmp("ignore", value)) + return CHECK_IGNORE; + if (!strcasecmp("warn", value)) + return CHECK_WARN; + if (!strcasecmp("error", value)) + return CHECK_ERROR; + warning(_("Unrecognized setting $check_level for option" + "rebase.missingCommitsCheck. Ignoring.")); + return CHECK_IGNORE; +} + +/* + * Check if the user dropped some commits by mistake + * Behaviour determined by rebase.missingCommitsCheck. + * Check if there is an unrecognized command or a + * bad SHA-1 in a command. + */ +int check_todo_list(void) +{ + enum check_level check_level = get_missing_commit_check_level(); + struct strbuf todo_file = STRBUF_INIT; + struct todo_list todo_list = TODO_LIST_INIT; + struct commit_list *missing = NULL; + int raise_error = 0, res = 0, fd, i; + + strbuf_addstr(&todo_file, rebase_path_todo()); + fd = open(todo_file.buf, O_RDONLY); + if (fd < 0) { + res = error_errno(_("Could not open %s"), todo_file.buf); + goto leave_check; + } + if (strbuf_read(&todo_list.buf, fd, 0) < 0) { + close(fd); + res = error(_("Could not read %s."), todo_file.buf); + goto leave_check; + } + close(fd); + raise_error = res = + parse_insn_buffer(todo_list.buf.buf, &todo_list); + + if (check_level == CHECK_IGNORE) + goto leave_check; + + /* Get the SHA-1 of the commits */ + for (i = 0; i < todo_list.nr; i++) { + struct commit *commit = todo_list.items[i].commit; + if (commit) + commit->util = todo_list.items + i; + } + + todo_list_release(&todo_list); + strbuf_addstr(&todo_file, ".backup"); + fd = open(todo_file.buf, O_RDONLY); + if (fd < 0) { + res = error_errno(_("Could not open %s"), todo_file.buf); + goto leave_check; + } + if (strbuf_read(&todo_list.buf, fd, 0) < 0) { + close(fd); + res = error(_("Could not read %s."), todo_file.buf); + goto leave_check; + } + close(fd); + strbuf_release(&todo_file); + res = !!parse_insn_buffer(todo_list.buf.buf, &todo_list); + + /* Find commits that are missing after editing */ + for (i = 0; i < todo_list.nr; i++) { + struct commit *commit = todo_list.items[i].commit; + if (commit && !commit->util) { + commit_list_insert(commit, &missing); + commit->util = todo_list.items + i; + } + } + + /* Warn about missing commits */ + if (!missing) + goto leave_check; + + if (check_level == CHECK_ERROR) + raise_error = res = 1; + + fprintf(stderr, + _("Warning: some commits may have been dropped accidentally.\n" + "Dropped commits (newer to older):\n")); + + /* Make the list user-friendly and display */ + while (missing) { + struct commit *commit = pop_commit(&missing); + struct todo_item *item = commit->util; + + fprintf(stderr, " - %s %.*s\n", short_commit_name(commit), + item->arg_len, item->arg); + } + free_commit_list(missing); + + fprintf(stderr, _("To avoid this message, use \"drop\" to " + "explicitly remove a commit.\n\n" + "Use 'git config rebase.missingCommitsCheck' to change " + "the level of warnings.\n" + "The possible behaviours are: ignore, warn, error.\n\n")); + +leave_check: + strbuf_release(&todo_file); + todo_list_release(&todo_list); + + if (raise_error) + fprintf(stderr, + _("You can fix this with 'git rebase --edit-todo'.\n" + "Or you can abort the rebase with 'git rebase" + " --abort'.\n")); + + return res; +} + +/* skip picking commits whose parents are unchanged */ +int skip_unnecessary_picks(void) +{ + const char *todo_file = rebase_path_todo(); + struct strbuf buf = STRBUF_INIT; + struct todo_list todo_list = TODO_LIST_INIT; + struct object_id onto_oid, *oid = &onto_oid, *parent_oid; + int fd, i; + + if (!read_oneliner(&buf, rebase_path_onto(), 0)) + return error("Could not read 'onto'"); + if (get_sha1(buf.buf, onto_oid.hash)) { + strbuf_release(&buf); + return error("Need a HEAD to fixup"); + } + strbuf_release(&buf); + + fd = open(todo_file, O_RDONLY); + if (fd < 0) { + 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); + } + close(fd); + if (parse_insn_buffer(todo_list.buf.buf, &todo_list) < 0) { + todo_list_release(&todo_list); + return -1; + } + + for (i = 0; i < todo_list.nr; i++) { + struct todo_item *item = todo_list.items + i; + + if (item->command >= TODO_NOOP) + continue; + if (item->command != TODO_PICK) + break; + if (parse_commit(item->commit)) { + todo_list_release(&todo_list); + return error(_("Could not parse commit '%s'"), + oid_to_hex(&item->commit->object.oid)); + } + if (!item->commit->parents) + break; /* root commit */ + if (item->commit->parents->next) + break; /* merge commit */ + parent_oid = &item->commit->parents->item->object.oid; + if (hashcmp(parent_oid->hash, oid->hash)) + break; + oid = &item->commit->object.oid; + } + if (i > 0) { + int offset = i < todo_list.nr ? + todo_list.items[i].offset_in_buf : todo_list.buf.len; + const char *done_path = rebase_path_done(); + + fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666); + if (write_in_full(fd, todo_list.buf.buf, offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("Could not write to '%s'"), + done_path); + } + close(fd); + + fd = open(rebase_path_todo(), O_WRONLY, 0666); + if (write_in_full(fd, todo_list.buf.buf + offset, + todo_list.buf.len - offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("Could not write to '%s'"), + rebase_path_todo()); + } + if (ftruncate(fd, todo_list.buf.len - offset) < 0) { + todo_list_release(&todo_list); + return error_errno(_("Could not truncate '%s'"), + rebase_path_todo()); + } + close(fd); + + todo_list.current = i; + if (is_fixup(peek_command(&todo_list, 0))) + record_in_rewritten(oid, peek_command(&todo_list, 0)); + } + + todo_list_release(&todo_list); + printf("%s\n", oid_to_hex(oid)); + + return 0; +} + +struct subject2item_entry { + struct hashmap_entry entry; + int i; + char subject[FLEX_ARRAY]; +}; + +static int subject2item_cmp(const struct subject2item_entry *a, + const struct subject2item_entry *b, const void *key) +{ + return key ? strcmp(a->subject, key) : strcmp(a->subject, b->subject); +} + +/* + * Rearrange the todo list that has both "pick sha1 msg" and "pick sha1 + * fixup!/squash! msg" in it so that the latter is put immediately after the + * former, and change "pick" to "fixup"/"squash". + * + * Note that if the config has specified a custom instruction format, each log + * message will have to be retrieved from the commit (as the oneline in the + * script cannot be trusted) in order to normalize the autosquash arrangement. + */ +int rearrange_squash(void) +{ + const char *todo_file = rebase_path_todo(); + struct todo_list todo_list = TODO_LIST_INIT; + struct hashmap subject2item; + int res = 0, rearranged = 0, *next, *tail, fd, i; + char **subjects; + + fd = open(todo_file, O_RDONLY); + if (fd < 0) + 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); + } + close(fd); + if (parse_insn_buffer(todo_list.buf.buf, &todo_list) < 0) { + todo_list_release(&todo_list); + return -1; + } + + /* + * The hashmap maps onelines to the respective todo list index. + * + * If any items need to be rearranged, the next[i] value will indicate + * which item was moved directly after the i'th. + * + * In that case, last[i] will indicate the index of the latest item to + * be moved to appear after the i'th. + */ + hashmap_init(&subject2item, (hashmap_cmp_fn) subject2item_cmp, + todo_list.nr); + ALLOC_ARRAY(next, todo_list.nr); + ALLOC_ARRAY(tail, todo_list.nr); + ALLOC_ARRAY(subjects, todo_list.nr); + for (i = 0; i < todo_list.nr; i++) { + struct strbuf buf = STRBUF_INIT; + struct todo_item *item = todo_list.items + i; + const char *commit_buffer, *subject, *p; + int i2 = -1; + struct subject2item_entry *entry; + + next[i] = tail[i] = -1; + if (item->command >= TODO_EXEC) { + subjects[i] = NULL; + continue; + } + + if (is_fixup(item->command)) { + todo_list_release(&todo_list); + return error(_("The script was already rearranged.")); + } + + item->commit->util = item; + + parse_commit(item->commit); + commit_buffer = get_commit_buffer(item->commit, NULL); + find_commit_subject(commit_buffer, &subject); + format_subject(&buf, subject, " "); + subject = subjects[i] = buf.buf; + unuse_commit_buffer(item->commit, commit_buffer); + if ((skip_prefix(subject, "fixup! ", &p) || + skip_prefix(subject, "squash! ", &p))) { + struct commit *commit2; + + for (;;) { + while (isspace(*p)) + p++; + if (!skip_prefix(p, "fixup! ", &p) && + !skip_prefix(p, "squash! ", &p)) + break; + } + + if ((entry = hashmap_get_from_hash(&subject2item, + strhash(p), p))) + /* found by title */ + i2 = entry->i; + else if (!strchr(p, ' ') && + (commit2 = + lookup_commit_reference_by_name(p)) && + commit2->util) + /* found by commit name */ + i2 = (struct todo_item *)commit2->util + - todo_list.items; + else { + /* copy can be a prefix of the commit subject */ + for (i2 = 0; i2 < i; i2++) + if (subjects[i2] && + starts_with(subjects[i2], p)) + break; + if (i2 == i) + i2 = -1; + } + } + if (i2 >= 0) { + rearranged = 1; + todo_list.items[i].command = + starts_with(subject, "fixup!") ? + TODO_FIXUP : TODO_SQUASH; + if (next[i2] < 0) + next[i2] = i; + else + next[tail[i2]] = i; + tail[i2] = i; + } + else if (!hashmap_get_from_hash(&subject2item, + strhash(subject), subject)) { + FLEX_ALLOC_MEM(entry, subject, buf.buf, buf.len); + entry->i = i; + hashmap_entry_init(entry, strhash(entry->subject)); + hashmap_put(&subject2item, entry); + } + strbuf_detach(&buf, NULL); + } + + if (rearranged) { + struct strbuf buf = STRBUF_INIT; + char *format = NULL; + + git_config_get_string("rebase.instructionFormat", &format); + for (i = 0; i < todo_list.nr; i++) { + enum todo_command command = todo_list.items[i].command; + int cur = i; + + /* + * Initially, all commands are 'pick's. If it is a + * fixup or a squash now, we have rearranged it. + */ + if (is_fixup(command)) + continue; + + while (cur >= 0) { + int offset = todo_list.items[cur].offset_in_buf; + int end_offset = cur + 1 < todo_list.nr ? + todo_list.items[cur + 1].offset_in_buf : + todo_list.buf.len; + char *bol = todo_list.buf.buf + offset; + char *eol = todo_list.buf.buf + end_offset; + + /* replace 'pick', by 'fixup' or 'squash' */ + command = todo_list.items[cur].command; + if (is_fixup(command)) { + strbuf_addstr(&buf, + todo_command_info[command].str); + bol += strcspn(bol, " \t"); + } + + strbuf_add(&buf, bol, eol - bol); + + cur = next[cur]; + } + } + + fd = open(todo_file, O_WRONLY); + if (fd < 0) + res |= error_errno(_("Could not open %s"), todo_file); + else if (write(fd, buf.buf, buf.len) < 0) + res |= error_errno(_("Could not read %s."), todo_file); + else if (ftruncate(fd, buf.len) < 0) + res |= error_errno(_("Could not finish %s"), todo_file); + close(fd); + strbuf_release(&buf); + } + + free(next); + free(tail); + for (i = 0; i < todo_list.nr; i++) + free(subjects[i]); + free(subjects); + hashmap_free(&subject2item, 1); + todo_list_release(&todo_list); + + return res; +} diff --git a/sequencer.h b/sequencer.h index 5ed5cb1d97ced7..a3aa3cbabc5432 100644 --- a/sequencer.h +++ b/sequencer.h @@ -1,28 +1,18 @@ #ifndef SEQUENCER_H #define SEQUENCER_H -#define SEQ_DIR "sequencer" -#define SEQ_HEAD_FILE "sequencer/head" -#define SEQ_TODO_FILE "sequencer/todo" -#define SEQ_OPTS_FILE "sequencer/opts" +const char *git_path_seq_dir(void); #define APPEND_SIGNOFF_DEDUP (1u << 0) enum replay_action { REPLAY_REVERT, - REPLAY_PICK -}; - -enum replay_subcommand { - REPLAY_NONE, - REPLAY_REMOVE_STATE, - REPLAY_CONTINUE, - REPLAY_ROLLBACK + REPLAY_PICK, + REPLAY_INTERACTIVE_REBASE }; struct replay_opts { enum replay_action action; - enum replay_subcommand subcommand; /* Boolean options */ int edit; @@ -34,6 +24,7 @@ struct replay_opts { int allow_empty; int allow_empty_message; int keep_redundant_commits; + int verbose; int mainline; @@ -46,9 +37,34 @@ struct replay_opts { /* Only used by REPLAY_NONE */ struct rev_info *revs; + + /* malloc()ed data entrusted to the sequencer */ + void **owned; + int owned_nr, owned_alloc; }; +#define REPLAY_OPTS_INIT { -1 } + +/* + * 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); +int sequencer_rollback(struct replay_opts *opts); +int sequencer_remove_state(struct replay_opts *opts); + +int sequencer_commit(const char *defmsg, struct replay_opts *opts, + int allow_empty, int edit, int amend, + int cleanup_commit_message); +int sequencer_make_script(int keep_empty, FILE *out, + int argc, const char **argv); + +int transform_todo_ids(int shorten_sha1s); +int check_todo_list(void); +int skip_unnecessary_picks(void); +int rearrange_squash(void); extern const char sign_off_header[]; diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 597e94e294d9dc..a18759eb0724b1 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1215,20 +1215,13 @@ test_expect_success 'rebase -i respects rebase.missingCommitsCheck = error' ' test B = $(git cat-file commit HEAD^ | sed -ne \$p) ' -cat >expect <actual && - test_i18ncmp expect actual && + test_i18ngrep "badcmd $(git rev-list --oneline -1 master~1)" actual && + test_i18ngrep "You can fix this with .git rebase --edit-todo.." actual && FAKE_LINES="1 2 3 drop 4 5" git rebase --edit-todo && git rebase --continue && test E = $(git cat-file commit HEAD | sed -ne \$p) && @@ -1250,20 +1243,13 @@ test_expect_success 'tabs and spaces are accepted in the todolist' ' test E = $(git cat-file commit HEAD | sed -ne \$p) ' -cat >expect <actual && - test_i18ncmp expect actual && + test_i18ngrep "edit XXXXXXX False commit" actual && + test_i18ngrep "You can fix this with .git rebase --edit-todo.." actual && FAKE_LINES="1 2 4 5 6" git rebase --edit-todo && git rebase --continue && test E = $(git cat-file commit HEAD | sed -ne \$p) diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 48346f1cc0c128..b9e26008a7934d 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -278,7 +278,7 @@ set_backup_editor () { test_set_editor "$PWD/backup-editor.sh" } -test_expect_failure 'autosquash with multiple empty patches' ' +test_expect_success 'autosquash with multiple empty patches' ' test_tick && git commit --allow-empty -m "empty" && test_tick && @@ -304,4 +304,18 @@ test_expect_success 'extra spaces after fixup!' ' test $base = $parent ' +test_expect_success 'wrapped original subject' ' + if test -d .git/rebase-merge; then git rebase --abort; fi && + base=$(git rev-parse HEAD) && + echo "wrapped subject" >wrapped && + git add wrapped && + test_tick && + git commit --allow-empty -m "$(printf "To\nfixup")" && + test_tick && + git commit --allow-empty -m "fixup! To fixup" && + git rebase -i --autosquash --keep-empty HEAD~2 && + parent=$(git rev-parse HEAD^) && + test $base = $parent +' + test_done diff --git a/wt-status.c b/wt-status.c index 539aac15a37adc..8e6c6e0fc7c56b 100644 --- a/wt-status.c +++ b/wt-status.c @@ -16,6 +16,7 @@ #include "strbuf.h" #include "utf8.h" #include "worktree.h" +#include "lockfile.h" static const char cut_line[] = "------------------------ >8 ------------------------\n"; @@ -2209,3 +2210,78 @@ void wt_status_print(struct wt_status *s) break; } } + +/** + * Returns 1 if there are unstaged changes, 0 otherwise. + */ +int has_unstaged_changes(int ignore_submodules) +{ + struct rev_info rev_info; + int result; + + init_revisions(&rev_info, NULL); + if (ignore_submodules) + DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES); + DIFF_OPT_SET(&rev_info.diffopt, QUICK); + diff_setup_done(&rev_info.diffopt); + result = run_diff_files(&rev_info, 0); + return diff_result_code(&rev_info.diffopt, result); +} + +/** + * Returns 1 if there are uncommitted changes, 0 otherwise. + */ +int has_uncommitted_changes(int ignore_submodules) +{ + struct rev_info rev_info; + int result; + + if (is_cache_unborn()) + return 0; + + init_revisions(&rev_info, NULL); + if (ignore_submodules) + DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES); + DIFF_OPT_SET(&rev_info.diffopt, QUICK); + add_head_to_pending(&rev_info); + diff_setup_done(&rev_info.diffopt); + result = run_diff_index(&rev_info, 1); + return diff_result_code(&rev_info.diffopt, result); +} + +/** + * If the work tree has unstaged or uncommitted changes, dies with the + * appropriate message. + */ +int require_clean_work_tree(const char *action, const char *hint, int ignore_submodules, int gently) +{ + struct lock_file *lock_file = xcalloc(1, sizeof(*lock_file)); + int err = 0; + + hold_locked_index(lock_file, 0); + refresh_cache(REFRESH_QUIET); + update_index_if_able(&the_index, lock_file); + rollback_lock_file(lock_file); + + if (has_unstaged_changes(ignore_submodules)) { + error(_("Cannot %s: You have unstaged changes."), action); + err = 1; + } + + if (has_uncommitted_changes(ignore_submodules)) { + if (err) + error(_("Additionally, your index contains uncommitted changes.")); + else + error(_("Cannot %s: Your index contains uncommitted changes."), action); + err = 1; + } + + if (err) { + if (hint) + error("%s", hint); + if (!gently) + exit(err); + } + + return err; +} diff --git a/wt-status.h b/wt-status.h index e40183770700cb..54fec77032dc9c 100644 --- a/wt-status.h +++ b/wt-status.h @@ -128,4 +128,10 @@ void status_printf_ln(struct wt_status *s, const char *color, const char *fmt, . __attribute__((format (printf, 3, 4))) void status_printf(struct wt_status *s, const char *color, const char *fmt, ...); +/* The following functions expect that the caller took care of reading the index. */ +int has_unstaged_changes(int ignore_submodules); +int has_uncommitted_changes(int ignore_submodules); +int require_clean_work_tree(const char *action, const char *hint, + int ignore_submodules, int gently); + #endif /* STATUS_H */