diff options
-rw-r--r-- | Documentation/config.txt | 11 | ||||
-rw-r--r-- | Documentation/git-rebase.txt | 9 | ||||
-rw-r--r-- | git-rebase--interactive.sh | 192 | ||||
-rw-r--r-- | t/lib-rebase.sh | 9 | ||||
-rwxr-xr-x | t/t3404-rebase-interactive.sh | 123 |
5 files changed, 338 insertions, 6 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt index 9358f4c16b..315f2710af 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2180,6 +2180,17 @@ rebase.autoStash:: successful rebase might result in non-trivial conflicts. Defaults to false. +rebase.missingCommitsCheck:: + If set to "warn", git rebase -i will print a warning if some + commits are removed (e.g. a line was deleted), however the + rebase will still proceed. If set to "error", it will print + the previous warning and stop the rebase, 'git rebase + --edit-todo' can then be used to correct the error. If set to + "ignore", no checking is done. + To drop a commit without warning or error, use the `drop` + command in the todo-list. + Defaults to "ignore". + rebase.instructionFormat A format string, as specified in linkgit:git-log[1], to be used for the instruction list during an interactive rebase. The format will automatically diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 7dc613cf3e..ca039546a4 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -213,6 +213,12 @@ rebase.autoSquash:: rebase.autoStash:: If set to true enable '--autostash' option by default. +rebase.missingCommitsCheck:: + If set to "warn", print warnings about removed commits in + interactive mode. If set to "error", print the warnings and + stop the rebase. If set to "ignore", no checking is + done. "ignore" by default. + rebase.instructionFormat:: Custom commit list format to use during an '--interactive' rebase. @@ -521,6 +527,9 @@ rebasing. If you just want to edit the commit message for a commit, replace the command "pick" with the command "reword". +To drop a commit, replace the command "pick" with "drop", or just +delete the matching line. + If you want to fold two or more commits into one, replace the command "pick" for the second and subsequent commits with "squash" or "fixup". If the commits had different authors, the folded commit will be diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 2f6ce55b58..f01637b1fd 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -152,11 +152,21 @@ Commands: 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. +EOF + if test $(get_missing_commit_check_level) = error + then + git stripspace --comment-lines >>"$todo" <<\EOF +Do not remove any line. Use 'drop' explicitly to remove a commit. +EOF + else + git stripspace --comment-lines >>"$todo" <<\EOF If you remove a line here THAT COMMIT WILL BE LOST. EOF + fi } make_patch () { @@ -505,7 +515,7 @@ do_next () { rm -f "$msg" "$author_script" "$amend" "$state_dir"/stopped-sha || exit read -r command sha1 rest < "$todo" case "$command" in - "$comment_char"*|''|noop) + "$comment_char"*|''|noop|drop|d) mark_action_done ;; pick|p) @@ -844,6 +854,180 @@ 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 to display if incorrect SHA-1 +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 + warn "Warning: the SHA-1 is missing or isn't" \ + "a commit in the following line:" + warn " - $2" + warn + fi + + return $badsha +} + +# prints the bad commits and bad commands +# from the todolist in stdin +check_bad_cmd_and_sha () { + retval=0 + git stripspace --strip-comments | + ( + while read -r line + do + IFS=' ' + set -- $line + command=$1 + sha1=$2 + + case $command in + ''|noop|x|"exec") + # Doesn't expect a SHA-1 + ;; + pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f) + if ! check_commit_sha $sha1 "$line" + then + retval=1 + fi + ;; + *) + warn "Warning: the command isn't recognized" \ + "in the following line:" + warn " - $line" + warn + retval=1 + ;; + esac + done + + 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" + output git checkout $onto || die_abort "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' +} + +# 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 "Warning: some commits may have been dropped" \ + "accidentally." + warn "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 "To avoid this message, use \"drop\" to" \ + "explicitly remove a commit." + warn + warn "Use 'git config rebase.missingCommitsCheck' to change" \ + "the level of warnings." + warn "The possible behaviours are: ignore, warn, error." + warn + fi + ;; + ignore) + ;; + *) + warn "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 "You can fix this with 'git rebase --edit-todo'." + die "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 @@ -1094,13 +1278,13 @@ git_sequence_editor "$todo" || has_action "$todo" || return 2 +check_todo_list + expand_todo_ids test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks -GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name" -output git checkout $onto || die_abort "could not detach HEAD" -git update-ref ORIG_HEAD $orig_head +checkout_onto do_rest } diff --git a/t/lib-rebase.sh b/t/lib-rebase.sh index 6bd252212a..9a96e1566d 100644 --- a/t/lib-rebase.sh +++ b/t/lib-rebase.sh @@ -14,7 +14,7 @@ # specified line. # # "<cmd> <lineno>" -- add a line with the specified command -# ("squash", "fixup", "edit", or "reword") and the SHA1 taken +# ("squash", "fixup", "edit", "reword" or "drop") and the SHA1 taken # from the specified line. # # "exec_cmd_with_args" -- add an "exec cmd with args" line. @@ -46,7 +46,7 @@ set_fake_editor () { action=pick for line in $FAKE_LINES; do case $line in - squash|fixup|edit|reword) + squash|fixup|edit|reword|drop) action="$line";; exec*) echo "$line" | sed 's/_/ /g' >> "$1";; @@ -54,6 +54,11 @@ set_fake_editor () { echo '# comment' >> "$1";; ">") echo >> "$1";; + bad) + action="badcmd";; + fakesha) + echo "$action XXXXXXX False commit" >> "$1" + action=pick;; *) sed -n "${line}s/^pick/$action/p" < "$1".tmp >> "$1" action=pick;; diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 467e6c1ed5..9d26064aba 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1123,4 +1123,127 @@ test_expect_success 'rebase --continue removes CHERRY_PICK_HEAD' ' test ! -f .git/CHERRY_PICK_HEAD ' +rebase_setup_and_clean () { + test_when_finished " + git checkout master && + test_might_fail git branch -D $1 && + test_might_fail git rebase --abort + " && + git checkout -b $1 master +} + +test_expect_success 'drop' ' + rebase_setup_and_clean drop-test && + set_fake_editor && + FAKE_LINES="1 drop 2 3 drop 4 5" git rebase -i --root && + test E = $(git cat-file commit HEAD | sed -ne \$p) && + test C = $(git cat-file commit HEAD^ | sed -ne \$p) && + test A = $(git cat-file commit HEAD^^ | sed -ne \$p) +' + +cat >expect <<EOF +Successfully rebased and updated refs/heads/missing-commit. +EOF + +test_expect_success 'rebase -i respects rebase.missingCommitsCheck = ignore' ' + test_config rebase.missingCommitsCheck ignore && + rebase_setup_and_clean missing-commit && + set_fake_editor && + FAKE_LINES="1 2 3 4" \ + git rebase -i --root 2>actual && + test D = $(git cat-file commit HEAD | sed -ne \$p) && + test_cmp expect actual +' + +cat >expect <<EOF +Warning: some commits may have been dropped accidentally. +Dropped commits (newer to older): + - $(git rev-list --pretty=oneline --abbrev-commit -1 master) +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. + +Successfully rebased and updated refs/heads/missing-commit. +EOF + +test_expect_success 'rebase -i respects rebase.missingCommitsCheck = warn' ' + test_config rebase.missingCommitsCheck warn && + rebase_setup_and_clean missing-commit && + set_fake_editor && + FAKE_LINES="1 2 3 4" \ + git rebase -i --root 2>actual && + test_cmp expect actual && + test D = $(git cat-file commit HEAD | sed -ne \$p) +' + +cat >expect <<EOF +Warning: some commits may have been dropped accidentally. +Dropped commits (newer to older): + - $(git rev-list --pretty=oneline --abbrev-commit -1 master) + - $(git rev-list --pretty=oneline --abbrev-commit -1 master~2) +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. + +You can fix this with 'git rebase --edit-todo'. +Or you can abort the rebase with 'git rebase --abort'. +EOF + +test_expect_success 'rebase -i respects rebase.missingCommitsCheck = error' ' + test_config rebase.missingCommitsCheck error && + rebase_setup_and_clean missing-commit && + set_fake_editor && + test_must_fail env FAKE_LINES="1 2 4" \ + git rebase -i --root 2>actual && + test_cmp expect actual && + cp .git/rebase-merge/git-rebase-todo.backup \ + .git/rebase-merge/git-rebase-todo && + FAKE_LINES="1 2 drop 3 4 drop 5" \ + git rebase --edit-todo && + git rebase --continue && + test D = $(git cat-file commit HEAD | sed -ne \$p) && + test B = $(git cat-file commit HEAD^ | sed -ne \$p) +' + +cat >expect <<EOF +Warning: the command isn't recognized in the following line: + - badcmd $(git rev-list --oneline -1 master~1) + +You can fix this with 'git rebase --edit-todo'. +Or you can abort the rebase with 'git rebase --abort'. +EOF + +test_expect_success 'static check of bad command' ' + rebase_setup_and_clean bad-cmd && + set_fake_editor && + test_must_fail env FAKE_LINES="1 2 3 bad 4 5" \ + git rebase -i --root 2>actual && + test_cmp expect 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) && + test C = $(git cat-file commit HEAD^ | sed -ne \$p) +' + +cat >expect <<EOF +Warning: the SHA-1 is missing or isn't a commit in the following line: + - edit XXXXXXX False commit + +You can fix this with 'git rebase --edit-todo'. +Or you can abort the rebase with 'git rebase --abort'. +EOF + +test_expect_success 'static check of bad SHA-1' ' + rebase_setup_and_clean bad-sha && + set_fake_editor && + test_must_fail env FAKE_LINES="1 2 edit fakesha 3 4 5 #" \ + git rebase -i --root 2>actual && + test_cmp expect 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) +' + test_done |