rebase -i: check for missing commits in the rebase--helper

In particular on Windows, where shell scripts are even more expensive than on MacOSX or Linux, it makes sense to move a loop that forks Git at least once for every line in the todo list into a builtin. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Jul 14, 2017 at 16:45 UTC 943999493fd4084d00bd5b5decc85b68eb64785f
4 files changed +134 -160
builtin/rebase--helper.c
+6 -1
@@ -14,7 +14,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
14 struct replay_opts opts = REPLAY_OPTS_INIT;
15 int keep_empty = 0;
16 enum {
17 - CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S
17 + CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S,
18 + CHECK_TODO_LIST
19 } command = 0;
20 struct option options[] = {
21 OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
@@ -29,6 +30,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
30 N_("shorten SHA-1s in the todo list"), SHORTEN_SHA1S),
31 OPT_CMDMODE(0, "expand-ids", &command,
32 N_("expand SHA-1s in the todo list"), EXPAND_SHA1S),
33 + OPT_CMDMODE(0, "check-todo-list", &command,
34 + N_("check the todo list"), CHECK_TODO_LIST),
35 OPT_END()
36 };
37
@@ -51,5 +54,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
54 return !!transform_todo_ids(1);
55 if (command == EXPAND_SHA1S && argc == 1)
56 return !!transform_todo_ids(0);
57 + if (command == CHECK_TODO_LIST && argc == 1)
58 + return !!check_todo_list();
59 usage_with_options(builtin_rebase_helper_usage, options);
60 }
git-rebase--interactive.sh
+5 -159
@@ -867,96 +867,6 @@ add_exec_commands () {
867 mv "$1.new" "$1"
868 }
869
870 -# Check if the SHA-1 passed as an argument is a
871 -# correct one, if not then print $2 in "$todo".badsha
872 -# $1: the SHA-1 to test
873 -# $2: the line number of the input
874 -# $3: the input filename
875 -check_commit_sha () {
876 - badsha=0
877 - if test -z "$1"
878 - then
879 - badsha=1
880 - else
881 - sha1_verif="$(git rev-parse --verify --quiet $1^{commit})"
882 - if test -z "$sha1_verif"
883 - then
884 - badsha=1
885 - fi
886 - fi
887 -
888 - if test $badsha -ne 0
889 - then
890 - line="$(sed -n -e "${2}p" "$3")"
891 - warn "$(eval_gettext "\
892 -Warning: the SHA-1 is missing or isn't a commit in the following line:
893 - - \$line")"
894 - warn
895 - fi
896 -
897 - return $badsha
898 -}
899 -
900 -# prints the bad commits and bad commands
901 -# from the todolist in stdin
902 -check_bad_cmd_and_sha () {
903 - retval=0
904 - lineno=0
905 - while read -r command rest
906 - do
907 - lineno=$(( $lineno + 1 ))
908 - case $command in
909 - "$comment_char"*|''|noop|x|exec)
910 - # Doesn't expect a SHA-1
911 - ;;
912 - "$cr")
913 - # Work around CR left by "read" (e.g. with Git for
914 - # Windows' Bash).
915 - ;;
916 - pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f)
917 - if ! check_commit_sha "${rest%%[ ]*}" "$lineno" "$1"
918 - then
919 - retval=1
920 - fi
921 - ;;
922 - *)
923 - line="$(sed -n -e "${lineno}p" "$1")"
924 - warn "$(eval_gettext "\
925 -Warning: the command isn't recognized in the following line:
926 - - \$line")"
927 - warn
928 - retval=1
929 - ;;
930 - esac
931 - done <"$1"
932 - return $retval
933 -}
934 -
935 -# Print the list of the SHA-1 of the commits
936 -# from stdin to stdout
937 -todo_list_to_sha_list () {
938 - git stripspace --strip-comments |
939 - while read -r command sha1 rest
940 - do
941 - case $command in
942 - "$comment_char"*|''|noop|x|"exec")
943 - ;;
944 - *)
945 - long_sha=$(git rev-list --no-walk "$sha1" 2>/dev/null)
946 - printf "%s\n" "$long_sha"
947 - ;;
948 - esac
949 - done
950 -}
951 -
952 -# Use warn for each line in stdin
953 -warn_lines () {
954 - while read -r line
955 - do
956 - warn " - $line"
957 - done
958 -}
959 -
870 # Switch to the branch in $into and notify it in the reflog
871 checkout_onto () {
872 GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name"
@@ -971,74 +881,6 @@ get_missing_commit_check_level () {
881 printf '%s' "$check_level" | tr 'A-Z' 'a-z'
882 }
883
974 -# Check if the user dropped some commits by mistake
975 -# Behaviour determined by rebase.missingCommitsCheck.
976 -# Check if there is an unrecognized command or a
977 -# bad SHA-1 in a command.
978 -check_todo_list () {
979 - raise_error=f
980 -
981 - check_level=$(get_missing_commit_check_level)
982 -
983 - case "$check_level" in
984 - warn|error)
985 - # Get the SHA-1 of the commits
986 - todo_list_to_sha_list <"$todo".backup >"$todo".oldsha1
987 - todo_list_to_sha_list <"$todo" >"$todo".newsha1
988 -
989 - # Sort the SHA-1 and compare them
990 - sort -u "$todo".oldsha1 >"$todo".oldsha1+
991 - mv "$todo".oldsha1+ "$todo".oldsha1
992 - sort -u "$todo".newsha1 >"$todo".newsha1+
993 - mv "$todo".newsha1+ "$todo".newsha1
994 - comm -2 -3 "$todo".oldsha1 "$todo".newsha1 >"$todo".miss
995 -
996 - # Warn about missing commits
997 - if test -s "$todo".miss
998 - then
999 - test "$check_level" = error && raise_error=t
1000 -
1001 - warn "$(gettext "\
1002 -Warning: some commits may have been dropped accidentally.
1003 -Dropped commits (newer to older):")"
1004 -
1005 - # Make the list user-friendly and display
1006 - opt="--no-walk=sorted --format=oneline --abbrev-commit --stdin"
1007 - git rev-list $opt <"$todo".miss | warn_lines
1008 -
1009 - warn "$(gettext "\
1010 -To avoid this message, use \"drop\" to explicitly remove a commit.
1011 -
1012 -Use 'git config rebase.missingCommitsCheck' to change the level of warnings.
1013 -The possible behaviours are: ignore, warn, error.")"
1014 - warn
1015 - fi
1016 - ;;
1017 - ignore)
1018 - ;;
1019 - *)
1020 - warn "$(eval_gettext "Unrecognized setting \$check_level for option rebase.missingCommitsCheck. Ignoring.")"
1021 - ;;
1022 - esac
1023 -
1024 - if ! check_bad_cmd_and_sha "$todo"
1025 - then
1026 - raise_error=t
1027 - fi
1028 -
1029 - if test $raise_error = t
1030 - then
1031 - # Checkout before the first commit of the
1032 - # rebase: this way git rebase --continue
1033 - # will work correctly as it expects HEAD to be
1034 - # placed before the commit of the next action
1035 - checkout_onto
1036 -
1037 - warn "$(gettext "You can fix this with 'git rebase --edit-todo' and then run 'git rebase --continue'.")"
1038 - die "$(gettext "Or you can abort the rebase with 'git rebase --abort'.")"
1039 - fi
1040 -}
1041 -
884 # The whole contents of this file is run by dot-sourcing it from
885 # inside a shell function. It used to be that "return"s we see
886 # below were not inside any function, and expected to return
@@ -1299,7 +1141,11 @@ git_sequence_editor "$todo" ||
1141 has_action "$todo" ||
1142 return 2
1143
1302 -check_todo_list
1144 +git rebase--helper --check-todo-list || {
1145 + ret=$?
1146 + checkout_onto
1147 + exit $ret
1148 +}
1149
1150 expand_todo_ids
1151
sequencer.c
+122
@@ -2519,3 +2519,125 @@ int transform_todo_ids(int shorten_ids)
2519 todo_list_release(&todo_list);
2520 return 0;
2521 }
2522 +
2523 +enum check_level {
2524 + CHECK_IGNORE = 0, CHECK_WARN, CHECK_ERROR
2525 +};
2526 +
2527 +static enum check_level get_missing_commit_check_level(void)
2528 +{
2529 + const char *value;
2530 +
2531 + if (git_config_get_value("rebase.missingcommitscheck", &value) ||
2532 + !strcasecmp("ignore", value))
2533 + return CHECK_IGNORE;
2534 + if (!strcasecmp("warn", value))
2535 + return CHECK_WARN;
2536 + if (!strcasecmp("error", value))
2537 + return CHECK_ERROR;
2538 + warning(_("unrecognized setting %s for option"
2539 + "rebase.missingCommitsCheck. Ignoring."), value);
2540 + return CHECK_IGNORE;
2541 +}
2542 +
2543 +/*
2544 + * Check if the user dropped some commits by mistake
2545 + * Behaviour determined by rebase.missingCommitsCheck.
2546 + * Check if there is an unrecognized command or a
2547 + * bad SHA-1 in a command.
2548 + */
2549 +int check_todo_list(void)
2550 +{
2551 + enum check_level check_level = get_missing_commit_check_level();
2552 + struct strbuf todo_file = STRBUF_INIT;
2553 + struct todo_list todo_list = TODO_LIST_INIT;
2554 + struct strbuf missing = STRBUF_INIT;
2555 + int advise_to_edit_todo = 0, res = 0, fd, i;
2556 +
2557 + strbuf_addstr(&todo_file, rebase_path_todo());
2558 + fd = open(todo_file.buf, O_RDONLY);
2559 + if (fd < 0) {
2560 + res = error_errno(_("could not open '%s'"), todo_file.buf);
2561 + goto leave_check;
2562 + }
2563 + if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
2564 + close(fd);
2565 + res = error(_("could not read '%s'."), todo_file.buf);
2566 + goto leave_check;
2567 + }
2568 + close(fd);
2569 + advise_to_edit_todo = res =
2570 + parse_insn_buffer(todo_list.buf.buf, &todo_list);
2571 +
2572 + if (res || check_level == CHECK_IGNORE)
2573 + goto leave_check;
2574 +
2575 + /* Mark the commits in git-rebase-todo as seen */
2576 + for (i = 0; i < todo_list.nr; i++) {
2577 + struct commit *commit = todo_list.items[i].commit;
2578 + if (commit)
2579 + commit->util = (void *)1;
2580 + }
2581 +
2582 + todo_list_release(&todo_list);
2583 + strbuf_addstr(&todo_file, ".backup");
2584 + fd = open(todo_file.buf, O_RDONLY);
2585 + if (fd < 0) {
2586 + res = error_errno(_("could not open '%s'"), todo_file.buf);
2587 + goto leave_check;
2588 + }
2589 + if (strbuf_read(&todo_list.buf, fd, 0) < 0) {
2590 + close(fd);
2591 + res = error(_("could not read '%s'."), todo_file.buf);
2592 + goto leave_check;
2593 + }
2594 + close(fd);
2595 + strbuf_release(&todo_file);
2596 + res = !!parse_insn_buffer(todo_list.buf.buf, &todo_list);
2597 +
2598 + /* Find commits in git-rebase-todo.backup yet unseen */
2599 + for (i = todo_list.nr - 1; i >= 0; i--) {
2600 + struct todo_item *item = todo_list.items + i;
2601 + struct commit *commit = item->commit;
2602 + if (commit && !commit->util) {
2603 + strbuf_addf(&missing, " - %s %.*s\n",
2604 + short_commit_name(commit),
2605 + item->arg_len, item->arg);
2606 + commit->util = (void *)1;
2607 + }
2608 + }
2609 +
2610 + /* Warn about missing commits */
2611 + if (!missing.len)
2612 + goto leave_check;
2613 +
2614 + if (check_level == CHECK_ERROR)
2615 + advise_to_edit_todo = res = 1;
2616 +
2617 + fprintf(stderr,
2618 + _("Warning: some commits may have been dropped accidentally.\n"
2619 + "Dropped commits (newer to older):\n"));
2620 +
2621 + /* Make the list user-friendly and display */
2622 + fputs(missing.buf, stderr);
2623 + strbuf_release(&missing);
2624 +
2625 + fprintf(stderr, _("To avoid this message, use \"drop\" to "
2626 + "explicitly remove a commit.\n\n"
2627 + "Use 'git config rebase.missingCommitsCheck' to change "
2628 + "the level of warnings.\n"
2629 + "The possible behaviours are: ignore, warn, error.\n\n"));
2630 +
2631 +leave_check:
2632 + strbuf_release(&todo_file);
2633 + todo_list_release(&todo_list);
2634 +
2635 + if (advise_to_edit_todo)
2636 + fprintf(stderr,
2637 + _("You can fix this with 'git rebase --edit-todo' "
2638 + "and then run 'git rebase --continue'.\n"
2639 + "Or you can abort the rebase with 'git rebase"
2640 + " --abort'.\n"));
2641 +
2642 + return res;
2643 +}
sequencer.h
+1
@@ -49,6 +49,7 @@ int sequencer_make_script(int keep_empty, FILE *out,
49 int argc, const char **argv);
50
51 int transform_todo_ids(int shorten_ids);
52 +int check_todo_list(void);
53
54 extern const char sign_off_header[];
55