diff options
author | Junio C Hamano <gitster@pobox.com> | 2018-10-11 14:18:19 +0900 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2018-10-11 14:18:19 +0900 |
commit | 5ab7e0fb67007bd4829b85a8b450bd952e97fe00 (patch) | |
tree | e9252517ae596c24d4cca1d924ddc0ff394f5238 | |
parent | builtin rebase: error out on incompatible option/mode combinations (diff) | |
parent | rebase -i: move rebase--helper modes to rebase--interactive (diff) | |
download | tgif-5ab7e0fb67007bd4829b85a8b450bd952e97fe00.tar.xz |
Merge branch 'ag/rebase-i-in-c' into js/rebase-in-c-5.5-work-with-rebase-i-in-c
* ag/rebase-i-in-c:
rebase -i: move rebase--helper modes to rebase--interactive
rebase -i: remove git-rebase--interactive.sh
rebase--interactive2: rewrite the submodes of interactive rebase in C
rebase -i: implement the main part of interactive rebase as a builtin
rebase -i: rewrite init_basic_state() in C
rebase -i: rewrite write_basic_state() in C
rebase -i: rewrite the rest of init_revisions_and_shortrevisions() in C
rebase -i: implement the logic to initialize $revisions in C
rebase -i: remove unused modes and functions
rebase -i: rewrite complete_action() in C
t3404: todo list with commented-out commands only aborts
sequencer: change the way skip_unnecessary_picks() returns its result
sequencer: refactor append_todo_help() to write its message to a buffer
rebase -i: rewrite checkout_onto() in C
rebase -i: rewrite setup_reflog_action() in C
sequencer: add a new function to silence a command, except if it fails
rebase -i: rewrite the edit-todo functionality in C
editor: add a function to launch the sequence editor
rebase -i: rewrite append_todo_help() in C
sequencer: make three functions and an enum from sequencer.c public
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | builtin.h | 2 | ||||
-rw-r--r-- | builtin/rebase--helper.c | 88 | ||||
-rw-r--r-- | builtin/rebase--interactive.c | 271 | ||||
-rw-r--r-- | cache.h | 1 | ||||
-rw-r--r-- | editor.c | 27 | ||||
-rwxr-xr-x | git-legacy-rebase.sh | 47 | ||||
-rw-r--r-- | git-rebase--interactive.sh | 283 | ||||
-rw-r--r-- | git-rebase--preserve-merges.sh | 10 | ||||
-rw-r--r-- | git.c | 2 | ||||
-rw-r--r-- | rebase-interactive.c | 90 | ||||
-rw-r--r-- | rebase-interactive.h | 8 | ||||
-rw-r--r-- | sequencer.c | 320 | ||||
-rw-r--r-- | sequencer.h | 22 | ||||
-rw-r--r-- | strbuf.h | 2 | ||||
-rwxr-xr-x | t/t3404-rebase-interactive.sh | 10 |
17 files changed, 740 insertions, 449 deletions
diff --git a/.gitignore b/.gitignore index 824141cba1..b94053f46b 100644 --- a/.gitignore +++ b/.gitignore @@ -118,7 +118,6 @@ /git-rebase /git-rebase--am /git-rebase--common -/git-rebase--helper /git-rebase--interactive /git-rebase--merge /git-rebase--preserve-merges @@ -620,7 +620,6 @@ SCRIPT_LIB += git-mergetool--lib SCRIPT_LIB += git-parse-remote SCRIPT_LIB += git-rebase--am SCRIPT_LIB += git-rebase--common -SCRIPT_LIB += git-rebase--interactive SCRIPT_LIB += git-rebase--preserve-merges SCRIPT_LIB += git-rebase--merge SCRIPT_LIB += git-sh-setup @@ -927,6 +926,7 @@ LIB_OBJS += protocol.o LIB_OBJS += quote.o LIB_OBJS += reachable.o LIB_OBJS += read-cache.o +LIB_OBJS += rebase-interactive.o LIB_OBJS += reflog-walk.o LIB_OBJS += refs.o LIB_OBJS += refs/files-backend.o @@ -1065,7 +1065,7 @@ BUILTIN_OBJS += builtin/pull.o BUILTIN_OBJS += builtin/push.o BUILTIN_OBJS += builtin/read-tree.o BUILTIN_OBJS += builtin/rebase.o -BUILTIN_OBJS += builtin/rebase--helper.o +BUILTIN_OBJS += builtin/rebase--interactive.o BUILTIN_OBJS += builtin/receive-pack.o BUILTIN_OBJS += builtin/reflog.o BUILTIN_OBJS += builtin/remote.o @@ -2404,7 +2404,6 @@ XGETTEXT_FLAGS_PERL = $(XGETTEXT_FLAGS) --language=Perl \ LOCALIZED_C = $(C_OBJ:o=c) $(LIB_H) $(GENERATED_H) LOCALIZED_SH = $(SCRIPT_SH) LOCALIZED_SH += git-parse-remote.sh -LOCALIZED_SH += git-rebase--interactive.sh LOCALIZED_SH += git-rebase--preserve-merges.sh LOCALIZED_SH += git-sh-setup.sh LOCALIZED_PERL = $(SCRIPT_PERL) @@ -203,7 +203,7 @@ 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(int argc, const char **argv, const char *prefix); -extern int cmd_rebase__helper(int argc, const char **argv, const char *prefix); +extern int cmd_rebase__interactive(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/rebase--helper.c b/builtin/rebase--helper.c deleted file mode 100644 index f7c2a5fdc8..0000000000 --- a/builtin/rebase--helper.c +++ /dev/null @@ -1,88 +0,0 @@ -#include "builtin.h" -#include "cache.h" -#include "config.h" -#include "parse-options.h" -#include "sequencer.h" - -static const char * const builtin_rebase_helper_usage[] = { - N_("git rebase--helper [<options>]"), - NULL -}; - -int cmd_rebase__helper(int argc, const char **argv, const char *prefix) -{ - struct replay_opts opts = REPLAY_OPTS_INIT; - unsigned flags = 0, keep_empty = 0, rebase_merges = 0; - int abbreviate_commands = 0, rebase_cousins = -1; - enum { - CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS, - CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH, - ADD_EXEC - } 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_BOOL(0, "allow-empty-message", &opts.allow_empty_message, - N_("allow commits with empty messages")), - OPT_BOOL(0, "rebase-merges", &rebase_merges, N_("rebase merge commits")), - OPT_BOOL(0, "rebase-cousins", &rebase_cousins, - N_("keep original branch points of cousins")), - 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-ids", &command, - N_("shorten commit ids in the todo list"), SHORTEN_OIDS), - OPT_CMDMODE(0, "expand-ids", &command, - N_("expand commit ids in the todo list"), EXPAND_OIDS), - 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_CMDMODE(0, "add-exec-commands", &command, - N_("insert exec commands in todo list"), ADD_EXEC), - OPT_END() - }; - - sequencer_init_config(&opts); - git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands); - - 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); - - flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0; - flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0; - flags |= rebase_merges ? TODO_LIST_REBASE_MERGES : 0; - flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0; - flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0; - - if (rebase_cousins >= 0 && !rebase_merges) - warning(_("--[no-]rebase-cousins has no effect without " - "--rebase-merges")); - - 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(stdout, argc, argv, flags); - if ((command == SHORTEN_OIDS || command == EXPAND_OIDS) && argc == 1) - return !!transform_todos(flags); - 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(); - if (command == ADD_EXEC && argc == 2) - return !!sequencer_add_exec_commands(argv[1]); - usage_with_options(builtin_rebase_helper_usage, options); -} diff --git a/builtin/rebase--interactive.c b/builtin/rebase--interactive.c new file mode 100644 index 0000000000..a2ab68ed06 --- /dev/null +++ b/builtin/rebase--interactive.c @@ -0,0 +1,271 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "parse-options.h" +#include "sequencer.h" +#include "rebase-interactive.h" +#include "argv-array.h" +#include "refs.h" +#include "rerere.h" +#include "run-command.h" + +static GIT_PATH_FUNC(path_state_dir, "rebase-merge/") +static GIT_PATH_FUNC(path_squash_onto, "rebase-merge/squash-onto") +static GIT_PATH_FUNC(path_interactive, "rebase-merge/interactive") + +static int get_revision_ranges(const char *upstream, const char *onto, + const char **head_hash, + char **revisions, char **shortrevisions) +{ + const char *base_rev = upstream ? upstream : onto, *shorthead; + struct object_id orig_head; + + if (get_oid("HEAD", &orig_head)) + return error(_("no HEAD?")); + + *head_hash = find_unique_abbrev(&orig_head, GIT_MAX_HEXSZ); + *revisions = xstrfmt("%s...%s", base_rev, *head_hash); + + shorthead = find_unique_abbrev(&orig_head, DEFAULT_ABBREV); + + if (upstream) { + const char *shortrev; + struct object_id rev_oid; + + get_oid(base_rev, &rev_oid); + shortrev = find_unique_abbrev(&rev_oid, DEFAULT_ABBREV); + + *shortrevisions = xstrfmt("%s..%s", shortrev, shorthead); + } else + *shortrevisions = xstrdup(shorthead); + + return 0; +} + +static int init_basic_state(struct replay_opts *opts, const char *head_name, + const char *onto, const char *orig_head) +{ + FILE *interactive; + + if (!is_directory(path_state_dir()) && mkdir_in_gitdir(path_state_dir())) + return error_errno(_("could not create temporary %s"), path_state_dir()); + + delete_reflog("REBASE_HEAD"); + + interactive = fopen(path_interactive(), "w"); + if (!interactive) + return error_errno(_("could not mark as interactive")); + fclose(interactive); + + return write_basic_state(opts, head_name, onto, orig_head); +} + +static int do_interactive_rebase(struct replay_opts *opts, unsigned flags, + const char *switch_to, const char *upstream, + const char *onto, const char *onto_name, + const char *squash_onto, const char *head_name, + const char *restrict_revision, char *raw_strategies, + const char *cmd, unsigned autosquash) +{ + int ret; + const char *head_hash = NULL; + char *revisions = NULL, *shortrevisions = NULL; + struct argv_array make_script_args = ARGV_ARRAY_INIT; + FILE *todo_list; + + if (prepare_branch_to_be_rebased(opts, switch_to)) + return -1; + + if (get_revision_ranges(upstream, onto, &head_hash, + &revisions, &shortrevisions)) + return -1; + + if (raw_strategies) + parse_strategy_opts(opts, raw_strategies); + + if (init_basic_state(opts, head_name, onto, head_hash)) { + free(revisions); + free(shortrevisions); + + return -1; + } + + if (!upstream && squash_onto) + write_file(path_squash_onto(), "%s\n", squash_onto); + + todo_list = fopen(rebase_path_todo(), "w"); + if (!todo_list) { + free(revisions); + free(shortrevisions); + + return error_errno(_("could not open %s"), rebase_path_todo()); + } + + argv_array_pushl(&make_script_args, "", revisions, NULL); + if (restrict_revision) + argv_array_push(&make_script_args, restrict_revision); + + ret = sequencer_make_script(todo_list, + make_script_args.argc, make_script_args.argv, + flags); + fclose(todo_list); + + if (ret) + error(_("could not generate todo list")); + else { + discard_cache(); + ret = complete_action(opts, flags, shortrevisions, onto_name, onto, + head_hash, cmd, autosquash); + } + + free(revisions); + free(shortrevisions); + argv_array_clear(&make_script_args); + + return ret; +} + +static const char * const builtin_rebase_interactive_usage[] = { + N_("git rebase--interactive [<options>]"), + NULL +}; + +int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + unsigned flags = 0, keep_empty = 0, rebase_merges = 0, autosquash = 0; + int abbreviate_commands = 0, rebase_cousins = -1, ret = 0; + const char *onto = NULL, *onto_name = NULL, *restrict_revision = NULL, + *squash_onto = NULL, *upstream = NULL, *head_name = NULL, + *switch_to = NULL, *cmd = NULL; + char *raw_strategies = NULL; + enum { + NONE = 0, CONTINUE, SKIP, EDIT_TODO, SHOW_CURRENT_PATCH, + SHORTEN_OIDS, EXPAND_OIDS, CHECK_TODO_LIST, REARRANGE_SQUASH, ADD_EXEC + } 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_BOOL(0, "allow-empty-message", &opts.allow_empty_message, + N_("allow commits with empty messages")), + OPT_BOOL(0, "rebase-merges", &rebase_merges, N_("rebase merge commits")), + OPT_BOOL(0, "rebase-cousins", &rebase_cousins, + N_("keep original branch points of cousins")), + OPT_BOOL(0, "autosquash", &autosquash, + N_("move commits that begin with squash!/fixup!")), + OPT_BOOL(0, "signoff", &opts.signoff, N_("sign commits")), + OPT__VERBOSE(&opts.verbose, N_("be verbose")), + OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), + CONTINUE), + OPT_CMDMODE(0, "skip", &command, N_("skip commit"), SKIP), + OPT_CMDMODE(0, "edit-todo", &command, N_("edit the todo list"), + EDIT_TODO), + OPT_CMDMODE(0, "show-current-patch", &command, N_("show the current patch"), + SHOW_CURRENT_PATCH), + OPT_CMDMODE(0, "shorten-ids", &command, + N_("shorten commit ids in the todo list"), SHORTEN_OIDS), + OPT_CMDMODE(0, "expand-ids", &command, + N_("expand commit ids in the todo list"), EXPAND_OIDS), + OPT_CMDMODE(0, "check-todo-list", &command, + N_("check the todo list"), CHECK_TODO_LIST), + OPT_CMDMODE(0, "rearrange-squash", &command, + N_("rearrange fixup/squash lines"), REARRANGE_SQUASH), + OPT_CMDMODE(0, "add-exec-commands", &command, + N_("insert exec commands in todo list"), ADD_EXEC), + OPT_STRING(0, "onto", &onto, N_("onto"), N_("onto")), + OPT_STRING(0, "restrict-revision", &restrict_revision, + N_("restrict-revision"), N_("restrict revision")), + OPT_STRING(0, "squash-onto", &squash_onto, N_("squash-onto"), + N_("squash onto")), + OPT_STRING(0, "upstream", &upstream, N_("upstream"), + N_("the upstream commit")), + OPT_STRING(0, "head-name", &head_name, N_("head-name"), N_("head name")), + { OPTION_STRING, 'S', "gpg-sign", &opts.gpg_sign, N_("key-id"), + N_("GPG-sign commits"), + PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_STRING(0, "strategy", &opts.strategy, N_("strategy"), + N_("rebase strategy")), + OPT_STRING(0, "strategy-opts", &raw_strategies, N_("strategy-opts"), + N_("strategy options")), + OPT_STRING(0, "switch-to", &switch_to, N_("switch-to"), + N_("the branch or commit to checkout")), + OPT_STRING(0, "onto-name", &onto_name, N_("onto-name"), N_("onto name")), + OPT_STRING(0, "cmd", &cmd, N_("cmd"), N_("the command to run")), + OPT_RERERE_AUTOUPDATE(&opts.allow_rerere_auto), + OPT_END() + }; + + sequencer_init_config(&opts); + git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands); + + opts.action = REPLAY_INTERACTIVE_REBASE; + opts.allow_ff = 1; + opts.allow_empty = 1; + + if (argc == 1) + usage_with_options(builtin_rebase_interactive_usage, options); + + argc = parse_options(argc, argv, NULL, options, + builtin_rebase_interactive_usage, PARSE_OPT_KEEP_ARGV0); + + opts.gpg_sign = xstrdup_or_null(opts.gpg_sign); + + flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0; + flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0; + flags |= rebase_merges ? TODO_LIST_REBASE_MERGES : 0; + flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0; + flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0; + + if (rebase_cousins >= 0 && !rebase_merges) + warning(_("--[no-]rebase-cousins has no effect without " + "--rebase-merges")); + + switch (command) { + case NONE: + if (!onto && !upstream) + die(_("a base commit must be provided with --upstream or --onto")); + + ret = do_interactive_rebase(&opts, flags, switch_to, upstream, onto, + onto_name, squash_onto, head_name, restrict_revision, + raw_strategies, cmd, autosquash); + break; + case SKIP: { + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + rerere_clear(&merge_rr); + /* fallthrough */ + case CONTINUE: + ret = sequencer_continue(&opts); + break; + } + case EDIT_TODO: + ret = edit_todo_list(flags); + break; + case SHOW_CURRENT_PATCH: { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + argv_array_pushl(&cmd.args, "show", "REBASE_HEAD", "--", NULL); + ret = run_command(&cmd); + + break; + } + case SHORTEN_OIDS: + case EXPAND_OIDS: + ret = transform_todos(flags); + break; + case CHECK_TODO_LIST: + ret = check_todo_list(); + break; + case REARRANGE_SQUASH: + ret = rearrange_squash(); + break; + case ADD_EXEC: + ret = sequencer_add_exec_commands(cmd); + break; + default: + BUG("invalid command '%d'", command); + } + + return !!ret; +} @@ -1472,6 +1472,7 @@ extern const char *fmt_name(const char *name, const char *email); extern const char *ident_default_name(void); extern const char *ident_default_email(void); extern const char *git_editor(void); +extern const char *git_sequence_editor(void); extern const char *git_pager(int stdout_is_tty); extern int is_terminal_dumb(void); extern int git_ident_config(const char *, const char *, void *); @@ -1,4 +1,5 @@ #include "cache.h" +#include "config.h" #include "strbuf.h" #include "run-command.h" #include "sigchain.h" @@ -34,10 +35,21 @@ const char *git_editor(void) return editor; } -int launch_editor(const char *path, struct strbuf *buffer, const char *const *env) +const char *git_sequence_editor(void) { - const char *editor = git_editor(); + const char *editor = getenv("GIT_SEQUENCE_EDITOR"); + + if (!editor) + git_config_get_string_const("sequence.editor", &editor); + if (!editor) + editor = git_editor(); + return editor; +} + +static int launch_specified_editor(const char *editor, const char *path, + struct strbuf *buffer, const char *const *env) +{ if (!editor) return error("Terminal is dumb, but EDITOR unset"); @@ -95,3 +107,14 @@ int launch_editor(const char *path, struct strbuf *buffer, const char *const *en return error_errno("could not read file '%s'", path); return 0; } + +int launch_editor(const char *path, struct strbuf *buffer, const char *const *env) +{ + return launch_specified_editor(git_editor(), path, buffer, env); +} + +int launch_sequence_editor(const char *path, struct strbuf *buffer, + const char *const *env) +{ + return launch_specified_editor(git_sequence_editor(), path, buffer, env); +} diff --git a/git-legacy-rebase.sh b/git-legacy-rebase.sh index af2cdfef03..7600765f54 100755 --- a/git-legacy-rebase.sh +++ b/git-legacy-rebase.sh @@ -135,26 +135,63 @@ finish_rebase () { rm -rf "$state_dir" } +run_interactive () { + GIT_CHERRY_PICK_HELP="$resolvemsg" + export GIT_CHERRY_PICK_HELP + + test -n "$keep_empty" && keep_empty="--keep-empty" + test -n "$rebase_merges" && rebase_merges="--rebase-merges" + test -n "$rebase_cousins" && rebase_cousins="--rebase-cousins" + test -n "$autosquash" && autosquash="--autosquash" + test -n "$verbose" && verbose="--verbose" + test -n "$force_rebase" && force_rebase="--no-ff" + test -n "$restrict_revision" && \ + restrict_revision="--restrict-revision=^$restrict_revision" + test -n "$upstream" && upstream="--upstream=$upstream" + test -n "$onto" && onto="--onto=$onto" + test -n "$squash_onto" && squash_onto="--squash-onto=$squash_onto" + test -n "$onto_name" && onto_name="--onto-name=$onto_name" + test -n "$head_name" && head_name="--head-name=$head_name" + test -n "$strategy" && strategy="--strategy=$strategy" + test -n "$strategy_opts" && strategy_opts="--strategy-opts=$strategy_opts" + test -n "$switch_to" && switch_to="--switch-to=$switch_to" + test -n "$cmd" && cmd="--cmd=$cmd" + test -n "$action" && action="--$action" + + exec git rebase--interactive "$action" "$keep_empty" "$rebase_merges" "$rebase_cousins" \ + "$upstream" "$onto" "$squash_onto" "$restrict_revision" \ + "$allow_empty_message" "$autosquash" "$verbose" \ + "$force_rebase" "$onto_name" "$head_name" "$strategy" \ + "$strategy_opts" "$cmd" "$switch_to" \ + "$allow_rerere_autoupdate" "$gpg_sign_opt" "$signoff" +} + run_specific_rebase () { if [ "$interactive_rebase" = implied ]; then GIT_EDITOR=: export GIT_EDITOR autosquash= fi - . git-rebase--$type - if test -z "$preserve_merges" + if test -n "$interactive_rebase" -a -z "$preserve_merges" then - git_rebase__$type + run_interactive else - git_rebase__preserve_merges + . git-rebase--$type + + if test -z "$preserve_merges" + then + git_rebase__$type + else + git_rebase__preserve_merges + fi fi ret=$? if test $ret -eq 0 then finish_rebase - elif test $ret -eq 2 # special exit status for rebase -i + elif test $ret -eq 2 # special exit status for rebase -p then apply_autostash && rm -rf "$state_dir" && diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh deleted file mode 100644 index 299ded2137..0000000000 --- a/git-rebase--interactive.sh +++ /dev/null @@ -1,283 +0,0 @@ -# This shell script fragment is sourced by git-rebase to implement -# its interactive mode. "git rebase --interactive" makes it easy -# to fix up commits in the middle of a series and rearrange commits. -# -# Copyright (c) 2006 Johannes E. Schindelin -# -# The original idea comes from Eric W. Biederman, in -# https://public-inbox.org/git/m1odwkyuf5.fsf_-_@ebiederm.dsl.xmission.com/ -# -# 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. -todo="$state_dir"/git-rebase-todo - -GIT_CHERRY_PICK_HELP="$resolvemsg" -export GIT_CHERRY_PICK_HELP - -comment_char=$(git config --get core.commentchar 2>/dev/null) -case "$comment_char" in -'' | auto) - comment_char="#" - ;; -?) - ;; -*) - comment_char=$(echo "$comment_char" | cut -c1) - ;; -esac - -orig_reflog_action="$GIT_REFLOG_ACTION" - -comment_for_reflog () { - case "$orig_reflog_action" in - ''|rebase*) - GIT_REFLOG_ACTION="rebase -i ($1)" - export GIT_REFLOG_ACTION - ;; - esac -} - -append_todo_help () { - gettext " -Commands: -p, pick <commit> = use commit -r, reword <commit> = use commit, but edit the commit message -e, edit <commit> = use commit, but stop for amending -s, squash <commit> = use commit, but meld into previous commit -f, fixup <commit> = like \"squash\", but discard this commit's log message -x, exec <command> = run command (the rest of the line) using shell -d, drop <commit> = remove commit -l, label <label> = label current HEAD with a name -t, reset <label> = reset HEAD to a label -m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] -. create a merge commit using the original merge commit's -. message (or the oneline, if no original merge commit was -. specified). Use -c <commit> to reword the commit message. - -These lines can be re-ordered; they are executed from top to bottom. -" | git stripspace --comment-lines >>"$todo" - - if test $(get_missing_commit_check_level) = error - then - gettext " -Do not remove any line. Use 'drop' explicitly to remove a commit. -" | git stripspace --comment-lines >>"$todo" - else - gettext " -If you remove a line here THAT COMMIT WILL BE LOST. -" | git stripspace --comment-lines >>"$todo" - fi -} - -die_abort () { - apply_autostash - rm -rf "$state_dir" - die "$1" -} - -has_action () { - test -n "$(git stripspace --strip-comments <"$1")" -} - -git_sequence_editor () { - if test -z "$GIT_SEQUENCE_EDITOR" - then - GIT_SEQUENCE_EDITOR="$(git config sequence.editor)" - if [ -z "$GIT_SEQUENCE_EDITOR" ] - then - GIT_SEQUENCE_EDITOR="$(git var GIT_EDITOR)" || return $? - fi - fi - - eval "$GIT_SEQUENCE_EDITOR" '"$@"' -} - -expand_todo_ids() { - git rebase--helper --expand-ids -} - -collapse_todo_ids() { - git rebase--helper --shorten-ids -} - -# Switch to the branch in $into and notify it in the reflog -checkout_onto () { - GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name" - output git checkout $onto || die_abort "$(gettext "could not detach HEAD")" - git update-ref ORIG_HEAD $orig_head -} - -get_missing_commit_check_level () { - check_level=$(git config --get rebase.missingCommitsCheck) - check_level=${check_level:-ignore} - # Don't be case sensitive - printf '%s' "$check_level" | tr 'A-Z' 'a-z' -} - -# Initiate an action. If the cannot be any -# further action it may exec a command -# or exit and not return. -# -# TODO: Consider a cleaner return model so it -# never exits and always return 0 if process -# is complete. -# -# Parameter 1 is the action to initiate. -# -# Returns 0 if the action was able to complete -# and if 1 if further processing is required. -initiate_action () { - case "$1" in - continue) - exec git rebase--helper ${force_rebase:+--no-ff} $allow_empty_message \ - --continue - ;; - skip) - git rerere clear - exec git rebase--helper ${force_rebase:+--no-ff} $allow_empty_message \ - --continue - ;; - edit-todo) - git stripspace --strip-comments <"$todo" >"$todo".new - mv -f "$todo".new "$todo" - collapse_todo_ids - append_todo_help - gettext " -You are editing the todo file of an ongoing interactive rebase. -To continue rebase after editing, run: - git rebase --continue - -" | git stripspace --comment-lines >>"$todo" - - git_sequence_editor "$todo" || - die "$(gettext "Could not execute editor")" - expand_todo_ids - - exit - ;; - show-current-patch) - exec git show REBASE_HEAD -- - ;; - *) - return 1 # continue - ;; - esac -} - -setup_reflog_action () { - comment_for_reflog start - - if test ! -z "$switch_to" - then - GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $switch_to" - output git checkout "$switch_to" -- || - die "$(eval_gettext "Could not checkout \$switch_to")" - - comment_for_reflog start - fi -} - -init_basic_state () { - orig_head=$(git rev-parse --verify HEAD) || die "$(gettext "No HEAD?")" - mkdir -p "$state_dir" || die "$(eval_gettext "Could not create temporary \$state_dir")" - rm -f "$(git rev-parse --git-path REBASE_HEAD)" - - : > "$state_dir"/interactive || die "$(gettext "Could not mark as interactive")" - write_basic_state -} - -init_revisions_and_shortrevisions () { - shorthead=$(git rev-parse --short $orig_head) - shortonto=$(git rev-parse --short $onto) - if test -z "$rebase_root" - # this is now equivalent to ! -z "$upstream" - then - shortupstream=$(git rev-parse --short $upstream) - revisions=$upstream...$orig_head - shortrevisions=$shortupstream..$shorthead - else - revisions=$onto...$orig_head - shortrevisions=$shorthead - test -z "$squash_onto" || - echo "$squash_onto" >"$state_dir"/squash-onto - fi -} - -complete_action() { - test -s "$todo" || echo noop >> "$todo" - test -z "$autosquash" || git rebase--helper --rearrange-squash || exit - test -n "$cmd" && git rebase--helper --add-exec-commands "$cmd" - - todocount=$(git stripspace --strip-comments <"$todo" | wc -l) - todocount=${todocount##* } - -cat >>"$todo" <<EOF - -$comment_char $(eval_ngettext \ - "Rebase \$shortrevisions onto \$shortonto (\$todocount command)" \ - "Rebase \$shortrevisions onto \$shortonto (\$todocount commands)" \ - "$todocount") -EOF - append_todo_help - gettext " - However, if you remove everything, the rebase will be aborted. - - " | git stripspace --comment-lines >>"$todo" - - if test -z "$keep_empty" - then - printf '%s\n' "$comment_char $(gettext "Note that empty commits are commented out")" >>"$todo" - fi - - - has_action "$todo" || - return 2 - - cp "$todo" "$todo".backup - collapse_todo_ids - git_sequence_editor "$todo" || - die_abort "$(gettext "Could not execute editor")" - - has_action "$todo" || - return 2 - - git rebase--helper --check-todo-list || { - ret=$? - checkout_onto - exit $ret - } - - expand_todo_ids - - test -n "$force_rebase" || - onto="$(git rebase--helper --skip-unnecessary-picks)" || - die "Could not skip unnecessary pick commands" - - checkout_onto - require_clean_work_tree "rebase" - exec git rebase--helper ${force_rebase:+--no-ff} $allow_empty_message \ - --continue -} - -git_rebase__interactive () { - initiate_action "$action" - ret=$? - if test $ret = 0; then - return 0 - fi - - setup_reflog_action - init_basic_state - - init_revisions_and_shortrevisions - - git rebase--helper --make-script ${keep_empty:+--keep-empty} \ - ${rebase_merges:+--rebase-merges} \ - ${rebase_cousins:+--rebase-cousins} \ - $revisions ${restrict_revision+^$restrict_revision} >"$todo" || - die "$(gettext "Could not generate todo list")" - - complete_action -} diff --git a/git-rebase--preserve-merges.sh b/git-rebase--preserve-merges.sh index c214c5e4d6..afbb65765d 100644 --- a/git-rebase--preserve-merges.sh +++ b/git-rebase--preserve-merges.sh @@ -711,11 +711,11 @@ do_rest () { } expand_todo_ids() { - git rebase--helper --expand-ids + git rebase--interactive --expand-ids } collapse_todo_ids() { - git rebase--helper --shorten-ids + git rebase--interactive --shorten-ids } # Switch to the branch in $into and notify it in the reflog @@ -876,8 +876,8 @@ init_revisions_and_shortrevisions () { complete_action() { test -s "$todo" || echo noop >> "$todo" - test -z "$autosquash" || git rebase--helper --rearrange-squash || exit - test -n "$cmd" && git rebase--helper --add-exec-commands "$cmd" + test -z "$autosquash" || git rebase--interactive --rearrange-squash || exit + test -n "$cmd" && git rebase--interactive --add-exec-commands --cmd "$cmd" todocount=$(git stripspace --strip-comments <"$todo" | wc -l) todocount=${todocount##* } @@ -912,7 +912,7 @@ However, if you remove everything, the rebase will be aborted. has_action "$todo" || return 2 - git rebase--helper --check-todo-list || { + git rebase--interactive --check-todo-list || { ret=$? checkout_onto exit $ret @@ -527,7 +527,7 @@ static struct cmd_struct commands[] = { * RUN_SETUP | NEED_WORK_TREE */ { "rebase", cmd_rebase }, - { "rebase--helper", cmd_rebase__helper, RUN_SETUP | NEED_WORK_TREE }, + { "rebase--interactive", cmd_rebase__interactive, RUN_SETUP | NEED_WORK_TREE }, { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, diff --git a/rebase-interactive.c b/rebase-interactive.c new file mode 100644 index 0000000000..0f4119cbae --- /dev/null +++ b/rebase-interactive.c @@ -0,0 +1,90 @@ +#include "cache.h" +#include "commit.h" +#include "rebase-interactive.h" +#include "sequencer.h" +#include "strbuf.h" + +void append_todo_help(unsigned edit_todo, unsigned keep_empty, + struct strbuf *buf) +{ + const char *msg = _("\nCommands:\n" +"p, pick <commit> = use commit\n" +"r, reword <commit> = use commit, but edit the commit message\n" +"e, edit <commit> = use commit, but stop for amending\n" +"s, squash <commit> = use commit, but meld into previous commit\n" +"f, fixup <commit> = like \"squash\", but discard this commit's log message\n" +"x, exec <command> = run command (the rest of the line) using shell\n" +"d, drop <commit> = remove commit\n" +"l, label <label> = label current HEAD with a name\n" +"t, reset <label> = reset HEAD to a label\n" +"m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]\n" +". create a merge commit using the original merge commit's\n" +". message (or the oneline, if no original merge commit was\n" +". specified). Use -c <commit> to reword the commit message.\n" +"\n" +"These lines can be re-ordered; they are executed from top to bottom.\n"); + + strbuf_add_commented_lines(buf, msg, strlen(msg)); + + if (get_missing_commit_check_level() == MISSING_COMMIT_CHECK_ERROR) + msg = _("\nDo not remove any line. Use 'drop' " + "explicitly to remove a commit.\n"); + else + msg = _("\nIf you remove a line here " + "THAT COMMIT WILL BE LOST.\n"); + + strbuf_add_commented_lines(buf, msg, strlen(msg)); + + if (edit_todo) + msg = _("\nYou are editing the todo file " + "of an ongoing interactive rebase.\n" + "To continue rebase after editing, run:\n" + " git rebase --continue\n\n"); + else + msg = _("\nHowever, if you remove everything, " + "the rebase will be aborted.\n\n"); + + strbuf_add_commented_lines(buf, msg, strlen(msg)); + + if (!keep_empty) { + msg = _("Note that empty commits are commented out"); + strbuf_add_commented_lines(buf, msg, strlen(msg)); + } +} + +int edit_todo_list(unsigned flags) +{ + struct strbuf buf = STRBUF_INIT; + const char *todo_file = rebase_path_todo(); + + if (strbuf_read_file(&buf, todo_file, 0) < 0) + return error_errno(_("could not read '%s'."), todo_file); + + strbuf_stripspace(&buf, 1); + if (write_message(buf.buf, buf.len, todo_file, 0)) { + strbuf_release(&buf); + return -1; + } + + strbuf_release(&buf); + + transform_todos(flags | TODO_LIST_SHORTEN_IDS); + + if (strbuf_read_file(&buf, todo_file, 0) < 0) + return error_errno(_("could not read '%s'."), todo_file); + + append_todo_help(1, 0, &buf); + if (write_message(buf.buf, buf.len, todo_file, 0)) { + strbuf_release(&buf); + return -1; + } + + strbuf_release(&buf); + + if (launch_sequence_editor(todo_file, NULL, NULL)) + return -1; + + transform_todos(flags & ~(TODO_LIST_SHORTEN_IDS)); + + return 0; +} diff --git a/rebase-interactive.h b/rebase-interactive.h new file mode 100644 index 0000000000..971da03776 --- /dev/null +++ b/rebase-interactive.h @@ -0,0 +1,8 @@ +#ifndef REBASE_INTERACTIVE_H +#define REBASE_INTERACTIVE_H + +void append_todo_help(unsigned edit_todo, unsigned keep_empty, + struct strbuf *buf); +int edit_todo_list(unsigned flags); + +#endif diff --git a/sequencer.c b/sequencer.c index 31038472fd..dfb6ad2f5b 100644 --- a/sequencer.c +++ b/sequencer.c @@ -30,6 +30,7 @@ #include "oidset.h" #include "commit-slab.h" #include "alias.h" +#include "rebase-interactive.h" #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" @@ -52,7 +53,10 @@ static GIT_PATH_FUNC(rebase_path, "rebase-merge") * 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") +GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo") +static GIT_PATH_FUNC(rebase_path_todo_backup, + "rebase-merge/git-rebase-todo.backup") + /* * The rebase command lines that have already been processed. A line * is moved here when it is first handled, before any associated user @@ -140,7 +144,7 @@ static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete") /* * The following files are written by git-rebase just after parsing the - * command-line (and are only consumed, not modified, by the sequencer). + * command-line. */ 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") @@ -152,6 +156,7 @@ 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") static GIT_PATH_FUNC(rebase_path_allow_rerere_autoupdate, "rebase-merge/allow_rerere_autoupdate") +static GIT_PATH_FUNC(rebase_path_quiet, "rebase-merge/quiet") static int git_sequencer_config(const char *k, const char *v, void *cb) { @@ -373,8 +378,8 @@ static void print_advice(int show_hint, struct replay_opts *opts) } } -static int write_message(const void *buf, size_t len, const char *filename, - int append_eol) +int write_message(const void *buf, size_t len, const char *filename, + int append_eol) { struct lock_file msg_file = LOCK_INIT; @@ -769,6 +774,23 @@ N_("you have staged changes in your working tree\n" #define VERIFY_MSG (1<<4) #define CREATE_ROOT_COMMIT (1<<5) +static int run_command_silent_on_success(struct child_process *cmd) +{ + struct strbuf buf = STRBUF_INIT; + int rc; + + cmd->stdout_to_stderr = 1; + rc = pipe_command(cmd, + NULL, 0, + NULL, 0, + &buf, 0); + + if (rc) + fputs(buf.buf, stderr); + strbuf_release(&buf); + return rc; +} + /* * If we are cherry-pick, and if the merge did not result in * hand-editing, we will hit this commit and inherit the original @@ -823,18 +845,11 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, cmd.git_cmd = 1; - if (is_rebase_i(opts)) { - if (!(flags & EDIT_MSG)) { - cmd.stdout_to_stderr = 1; - cmd.err = -1; - } - - if (read_env_script(&cmd.env_array)) { - const char *gpg_opt = gpg_sign_opt_quoted(opts); + if (is_rebase_i(opts) && read_env_script(&cmd.env_array)) { + const char *gpg_opt = gpg_sign_opt_quoted(opts); - return error(_(staged_changes_advice), - gpg_opt, gpg_opt); - } + return error(_(staged_changes_advice), + gpg_opt, gpg_opt); } argv_array_push(&cmd.args, "commit"); @@ -864,21 +879,10 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, if (opts->allow_empty_message) argv_array_push(&cmd.args, "--allow-empty-message"); - if (cmd.err == -1) { - /* hide stderr on success */ - struct strbuf buf = STRBUF_INIT; - int rc = pipe_command(&cmd, - NULL, 0, - /* stdout is already redirected */ - NULL, 0, - &buf, 0); - if (rc) - fputs(buf.buf, stderr); - strbuf_release(&buf); - return rc; - } - - return run_command(&cmd); + if (is_rebase_i(opts) && !(flags & EDIT_MSG)) + return run_command_silent_on_success(&cmd); + else + return run_command(&cmd); } static int rest_is_empty(const struct strbuf *sb, int start) @@ -2202,21 +2206,14 @@ static int populate_opts_cb(const char *key, const char *value, void *data) return 0; } -static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf) +void parse_strategy_opts(struct replay_opts *opts, char *raw_opts) { int i; - char *strategy_opts_string; - - strbuf_reset(buf); - if (!read_oneliner(buf, rebase_path_strategy(), 0)) - return; - opts->strategy = strbuf_detach(buf, NULL); - if (!read_oneliner(buf, rebase_path_strategy_opts(), 0)) - return; + char *strategy_opts_string = raw_opts; - strategy_opts_string = buf->buf; if (*strategy_opts_string == ' ') strategy_opts_string++; + opts->xopts_nr = split_cmdline(strategy_opts_string, (const char ***)&opts->xopts); for (i = 0; i < opts->xopts_nr; i++) { @@ -2227,6 +2224,18 @@ static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf) } } +static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf) +{ + strbuf_reset(buf); + if (!read_oneliner(buf, rebase_path_strategy(), 0)) + return; + opts->strategy = strbuf_detach(buf, NULL); + if (!read_oneliner(buf, rebase_path_strategy_opts(), 0)) + return; + + parse_strategy_opts(opts, buf->buf); +} + static int read_populate_opts(struct replay_opts *opts) { if (is_rebase_i(opts)) { @@ -2294,6 +2303,55 @@ static int read_populate_opts(struct replay_opts *opts) return 0; } +static void write_strategy_opts(struct replay_opts *opts) +{ + int i; + struct strbuf buf = STRBUF_INIT; + + for (i = 0; i < opts->xopts_nr; ++i) + strbuf_addf(&buf, " --%s", opts->xopts[i]); + + write_file(rebase_path_strategy_opts(), "%s\n", buf.buf); + strbuf_release(&buf); +} + +int write_basic_state(struct replay_opts *opts, const char *head_name, + const char *onto, const char *orig_head) +{ + const char *quiet = getenv("GIT_QUIET"); + + if (head_name) + write_file(rebase_path_head_name(), "%s\n", head_name); + if (onto) + write_file(rebase_path_onto(), "%s\n", onto); + if (orig_head) + write_file(rebase_path_orig_head(), "%s\n", orig_head); + + if (quiet) + write_file(rebase_path_quiet(), "%s\n", quiet); + else + write_file(rebase_path_quiet(), "\n"); + + if (opts->verbose) + write_file(rebase_path_verbose(), ""); + if (opts->strategy) + write_file(rebase_path_strategy(), "%s\n", opts->strategy); + if (opts->xopts_nr > 0) + write_strategy_opts(opts); + + if (opts->allow_rerere_auto == RERERE_AUTOUPDATE) + write_file(rebase_path_allow_rerere_autoupdate(), "--rerere-autoupdate\n"); + else if (opts->allow_rerere_auto == RERERE_NOAUTOUPDATE) + write_file(rebase_path_allow_rerere_autoupdate(), "--no-rerere-autoupdate\n"); + + if (opts->gpg_sign) + write_file(rebase_path_gpg_sign_opt(), "-S%s\n", opts->gpg_sign); + if (opts->signoff) + write_file(rebase_path_signoff(), "--signoff\n"); + + return 0; +} + static int walk_revs_populate_todo(struct todo_list *todo_list, struct replay_opts *opts) { @@ -3228,6 +3286,55 @@ static const char *reflog_message(struct replay_opts *opts, return buf.buf; } +static int run_git_checkout(struct replay_opts *opts, const char *commit, + const char *action) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + + argv_array_push(&cmd.args, "checkout"); + argv_array_push(&cmd.args, commit); + argv_array_pushf(&cmd.env_array, GIT_REFLOG_ACTION "=%s", action); + + if (opts->verbose) + return run_command(&cmd); + else + return run_command_silent_on_success(&cmd); +} + +int prepare_branch_to_be_rebased(struct replay_opts *opts, const char *commit) +{ + const char *action; + + if (commit && *commit) { + action = reflog_message(opts, "start", "checkout %s", commit); + if (run_git_checkout(opts, commit, action)) + return error(_("could not checkout %s"), commit); + } + + return 0; +} + +static int checkout_onto(struct replay_opts *opts, + const char *onto_name, const char *onto, + const char *orig_head) +{ + struct object_id oid; + const char *action = reflog_message(opts, "start", "checkout %s", onto_name); + + if (get_oid(orig_head, &oid)) + return error(_("%s: not a valid OID"), orig_head); + + if (run_git_checkout(opts, onto, action)) { + apply_autostash(opts); + sequencer_remove_state(opts); + return error(_("could not detach HEAD")); + } + + return update_ref(NULL, "ORIG_HEAD", &oid, NULL, 0, UPDATE_REFS_MSG_ON_ERR); +} + static const char rescheduled_advice[] = N_("Could not execute the todo command\n" "\n" @@ -4333,24 +4440,20 @@ int transform_todos(unsigned flags) return i; } -enum check_level { - CHECK_IGNORE = 0, CHECK_WARN, CHECK_ERROR -}; - -static enum check_level get_missing_commit_check_level(void) +enum missing_commit_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; + return MISSING_COMMIT_CHECK_IGNORE; if (!strcasecmp("warn", value)) - return CHECK_WARN; + return MISSING_COMMIT_CHECK_WARN; if (!strcasecmp("error", value)) - return CHECK_ERROR; + return MISSING_COMMIT_CHECK_ERROR; warning(_("unrecognized setting %s for option " "rebase.missingCommitsCheck. Ignoring."), value); - return CHECK_IGNORE; + return MISSING_COMMIT_CHECK_IGNORE; } define_commit_slab(commit_seen, unsigned char); @@ -4362,7 +4465,7 @@ define_commit_slab(commit_seen, unsigned char); */ int check_todo_list(void) { - enum check_level check_level = get_missing_commit_check_level(); + enum missing_commit_check_level check_level = get_missing_commit_check_level(); struct strbuf todo_file = STRBUF_INIT; struct todo_list todo_list = TODO_LIST_INIT; struct strbuf missing = STRBUF_INIT; @@ -4379,7 +4482,7 @@ int check_todo_list(void) advise_to_edit_todo = res = parse_insn_buffer(todo_list.buf.buf, &todo_list); - if (res || check_level == CHECK_IGNORE) + if (res || check_level == MISSING_COMMIT_CHECK_IGNORE) goto leave_check; /* Mark the commits in git-rebase-todo as seen */ @@ -4414,7 +4517,7 @@ int check_todo_list(void) if (!missing.len) goto leave_check; - if (check_level == CHECK_ERROR) + if (check_level == MISSING_COMMIT_CHECK_ERROR) advise_to_edit_todo = res = 1; fprintf(stderr, @@ -4460,17 +4563,17 @@ static int rewrite_file(const char *path, const char *buf, size_t len) } /* skip picking commits whose parents are unchanged */ -int skip_unnecessary_picks(void) +static int skip_unnecessary_picks(struct object_id *output_oid) { 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; + struct object_id *parent_oid; int fd, i; if (!read_oneliner(&buf, rebase_path_onto(), 0)) return error(_("could not read 'onto'")); - if (get_oid(buf.buf, &onto_oid)) { + if (get_oid(buf.buf, output_oid)) { strbuf_release(&buf); return error(_("need a HEAD to fixup")); } @@ -4500,9 +4603,9 @@ int skip_unnecessary_picks(void) if (item->commit->parents->next) break; /* merge commit */ parent_oid = &item->commit->parents->item->object.oid; - if (hashcmp(parent_oid->hash, oid->hash)) + if (hashcmp(parent_oid->hash, output_oid->hash)) break; - oid = &item->commit->object.oid; + oidcpy(output_oid, &item->commit->object.oid); } if (i > 0) { int offset = get_item_line_offset(&todo_list, i); @@ -4531,15 +4634,114 @@ int skip_unnecessary_picks(void) todo_list.current = i; if (is_fixup(peek_command(&todo_list, 0))) - record_in_rewritten(oid, peek_command(&todo_list, 0)); + record_in_rewritten(output_oid, peek_command(&todo_list, 0)); } todo_list_release(&todo_list); - printf("%s\n", oid_to_hex(oid)); return 0; } +int complete_action(struct replay_opts *opts, unsigned flags, + const char *shortrevisions, const char *onto_name, + const char *onto, const char *orig_head, const char *cmd, + unsigned autosquash) +{ + const char *shortonto, *todo_file = rebase_path_todo(); + struct todo_list todo_list = TODO_LIST_INIT; + struct strbuf *buf = &(todo_list.buf); + struct object_id oid; + struct stat st; + + get_oid(onto, &oid); + shortonto = find_unique_abbrev(&oid, DEFAULT_ABBREV); + + if (!lstat(todo_file, &st) && st.st_size == 0 && + write_message("noop\n", 5, todo_file, 0)) + return -1; + + if (autosquash && rearrange_squash()) + return -1; + + if (cmd && *cmd) + sequencer_add_exec_commands(cmd); + + if (strbuf_read_file(buf, todo_file, 0) < 0) + return error_errno(_("could not read '%s'."), todo_file); + + if (parse_insn_buffer(buf->buf, &todo_list)) { + todo_list_release(&todo_list); + return error(_("unusable todo list: '%s'"), todo_file); + } + + if (count_commands(&todo_list) == 0) { + apply_autostash(opts); + sequencer_remove_state(opts); + todo_list_release(&todo_list); + + return error(_("nothing to do")); + } + + strbuf_addch(buf, '\n'); + strbuf_commented_addf(buf, Q_("Rebase %s onto %s (%d command)", + "Rebase %s onto %s (%d commands)", + count_commands(&todo_list)), + shortrevisions, shortonto, count_commands(&todo_list)); + append_todo_help(0, flags & TODO_LIST_KEEP_EMPTY, buf); + + if (write_message(buf->buf, buf->len, todo_file, 0)) { + todo_list_release(&todo_list); + return -1; + } + + if (copy_file(rebase_path_todo_backup(), todo_file, 0666)) + return error(_("could not copy '%s' to '%s'."), todo_file, + rebase_path_todo_backup()); + + if (transform_todos(flags | TODO_LIST_SHORTEN_IDS)) + return error(_("could not transform the todo list")); + + strbuf_reset(buf); + + if (launch_sequence_editor(todo_file, buf, NULL)) { + apply_autostash(opts); + sequencer_remove_state(opts); + todo_list_release(&todo_list); + + return -1; + } + + strbuf_stripspace(buf, 1); + if (buf->len == 0) { + apply_autostash(opts); + sequencer_remove_state(opts); + todo_list_release(&todo_list); + + return error(_("nothing to do")); + } + + todo_list_release(&todo_list); + + if (check_todo_list()) { + checkout_onto(opts, onto_name, onto, orig_head); + return -1; + } + + if (transform_todos(flags & ~(TODO_LIST_SHORTEN_IDS))) + return error(_("could not transform the todo list")); + + if (opts->allow_ff && skip_unnecessary_picks(&oid)) + return error(_("could not skip unnecessary pick commands")); + + if (checkout_onto(opts, onto_name, oid_to_hex(&oid), orig_head)) + return -1; +; + if (require_clean_work_tree("rebase", "", 1, 1)) + return -1; + + return sequencer_continue(opts); +} + struct subject2item_entry { struct hashmap_entry entry; int i; diff --git a/sequencer.h b/sequencer.h index c5787c6b56..aab280f276 100644 --- a/sequencer.h +++ b/sequencer.h @@ -3,6 +3,7 @@ const char *git_path_commit_editmsg(void); const char *git_path_seq_dir(void); +const char *rebase_path_todo(void); #define APPEND_SIGNOFF_DEDUP (1u << 0) @@ -57,6 +58,15 @@ struct replay_opts { }; #define REPLAY_OPTS_INIT { .action = -1, .current_fixups = STRBUF_INIT } +enum missing_commit_check_level { + MISSING_COMMIT_CHECK_IGNORE = 0, + MISSING_COMMIT_CHECK_WARN, + MISSING_COMMIT_CHECK_ERROR +}; + +int write_message(const void *buf, size_t len, const char *filename, + int append_eol); + /* Call this to setup defaults before parsing command line options */ void sequencer_init_config(struct replay_opts *opts); int sequencer_pick_revisions(struct replay_opts *opts); @@ -79,8 +89,12 @@ int sequencer_make_script(FILE *out, int argc, const char **argv, int sequencer_add_exec_commands(const char *command); int transform_todos(unsigned flags); +enum missing_commit_check_level get_missing_commit_check_level(void); int check_todo_list(void); -int skip_unnecessary_picks(void); +int complete_action(struct replay_opts *opts, unsigned flags, + const char *shortrevisions, const char *onto_name, + const char *onto, const char *orig_head, const char *cmd, + unsigned autosquash); int rearrange_squash(void); extern const char sign_off_header[]; @@ -98,8 +112,14 @@ int update_head_with_reflog(const struct commit *old_head, void commit_post_rewrite(const struct commit *current_head, const struct object_id *new_head); +int prepare_branch_to_be_rebased(struct replay_opts *opts, const char *commit); + #define SUMMARY_INITIAL_COMMIT (1 << 0) #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1) void print_commit_summary(const char *prefix, const struct object_id *oid, unsigned int flags); #endif + +void parse_strategy_opts(struct replay_opts *opts, char *raw_opts); +int write_basic_state(struct replay_opts *opts, const char *head_name, + const char *onto, const char *orig_head); @@ -578,6 +578,8 @@ extern void strbuf_add_unique_abbrev(struct strbuf *sb, * file's contents are not read into the buffer upon completion. */ extern int launch_editor(const char *path, struct strbuf *buffer, const char *const *env); +extern int launch_sequence_editor(const char *path, struct strbuf *buffer, + const char *const *env); extern void strbuf_add_lines(struct strbuf *sb, const char *prefix, const char *buf, size_t size); diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 640fddc9c3..a147e2f1d4 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -75,6 +75,16 @@ test_expect_success 'rebase --keep-empty' ' test_line_count = 6 actual ' +cat > expect <<EOF +error: nothing to do +EOF + +test_expect_success 'rebase -i with empty HEAD' ' + set_fake_editor && + test_must_fail env FAKE_LINES="1 exec_true" git rebase -i HEAD^ >actual 2>&1 && + test_i18ncmp expect actual +' + test_expect_success 'rebase -i with the exec command' ' git checkout master && ( |