diff options
author | Junio C Hamano <gitster@pobox.com> | 2018-07-24 14:50:43 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2018-07-24 14:50:43 -0700 |
commit | 0ce5a698c6f775c2cc18de9eb62d558b1b85d752 (patch) | |
tree | fc06e3b923f1bbf2f358bca41d4cdb9d699753e3 | |
parent | Merge branch 'sb/submodule-move-head-error-msg' (diff) | |
parent | git-rebase: make --allow-empty-message the default (diff) | |
download | tgif-0ce5a698c6f775c2cc18de9eb62d558b1b85d752.tar.xz |
Merge branch 'en/rebase-consistency'
"git rebase" behaved slightly differently depending on which one of
the three backends gets used; this has been documented and an
effort to make them more uniform has begun.
* en/rebase-consistency:
git-rebase: make --allow-empty-message the default
t3401: add directory rename testcases for rebase and am
git-rebase.txt: document behavioral differences between modes
directory-rename-detection.txt: technical docs on abilities and limitations
git-rebase.txt: address confusion between --no-ff vs --force-rebase
git-rebase: error out when incompatible options passed
t3422: new testcases for checking when incompatible options passed
git-rebase.sh: update help messages a bit
git-rebase.txt: document incompatible options
-rw-r--r-- | Documentation/git-rebase.txt | 135 | ||||
-rw-r--r-- | Documentation/technical/directory-rename-detection.txt | 115 | ||||
-rwxr-xr-x | git-rebase.sh | 43 | ||||
-rwxr-xr-x | t/t3401-rebase-and-am-rename.sh | 105 | ||||
-rwxr-xr-x | t/t3404-rebase-interactive.sh | 7 | ||||
-rwxr-xr-x | t/t3405-rebase-malformed.sh | 11 | ||||
-rwxr-xr-x | t/t3422-rebase-incompatible-options.sh | 88 |
7 files changed, 462 insertions, 42 deletions
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 091eb53faa..a7850415b1 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -243,11 +243,15 @@ leave out at most one of A and B, in which case it defaults to HEAD. --keep-empty:: Keep the commits that do not change anything from its parents in the result. ++ +See also INCOMPATIBLE OPTIONS below. --allow-empty-message:: By default, rebasing commits with an empty message will fail. This option overrides that behavior, allowing commits with empty messages to be rebased. ++ +See also INCOMPATIBLE OPTIONS below. --skip:: Restart the rebasing process by skipping the current patch. @@ -271,6 +275,8 @@ branch on top of the <upstream> branch. Because of this, when a merge conflict happens, the side reported as 'ours' is the so-far rebased series, starting with <upstream>, and 'theirs' is the working branch. In other words, the sides are swapped. ++ +See also INCOMPATIBLE OPTIONS below. -s <strategy>:: --strategy=<strategy>:: @@ -280,8 +286,10 @@ other words, the sides are swapped. + Because 'git rebase' replays each commit from the working branch on top of the <upstream> branch using the given strategy, using -the 'ours' strategy simply discards all patches from the <branch>, +the 'ours' strategy simply empties all patches from the <branch>, which makes little sense. ++ +See also INCOMPATIBLE OPTIONS below. -X <strategy-option>:: --strategy-option=<strategy-option>:: @@ -289,6 +297,8 @@ which makes little sense. This implies `--merge` and, if no strategy has been specified, `-s recursive`. Note the reversal of 'ours' and 'theirs' as noted above for the `-m` option. ++ +See also INCOMPATIBLE OPTIONS below. -S[<keyid>]:: --gpg-sign[=<keyid>]:: @@ -324,17 +334,21 @@ which makes little sense. and after each change. When fewer lines of surrounding context exist they all must match. By default no context is ever ignored. ++ +See also INCOMPATIBLE OPTIONS below. --f:: +--no-ff:: --force-rebase:: - Force a rebase even if the current branch is up to date and - the command without `--force` would return without doing anything. +-f:: + Individually replay all rebased commits instead of fast-forwarding + over the unchanged ones. This ensures that the entire history of + the rebased branch is composed of new commits. + -You may find this (or --no-ff with an interactive rebase) helpful after -reverting a topic branch merge, as this option recreates the topic branch with -fresh commits so it can be remerged successfully without needing to "revert -the reversion" (see the -link:howto/revert-a-faulty-merge.html[revert-a-faulty-merge How-To] for details). +You may find this helpful after reverting a topic branch merge, as this option +recreates the topic branch with fresh commits so it can be remerged +successfully without needing to "revert the reversion" (see the +link:howto/revert-a-faulty-merge.html[revert-a-faulty-merge How-To] for +details). --fork-point:: --no-fork-point:: @@ -355,19 +369,22 @@ default is `--no-fork-point`, otherwise the default is `--fork-point`. --whitespace=<option>:: These flag are passed to the 'git apply' program (see linkgit:git-apply[1]) that applies the patch. - Incompatible with the --interactive option. ++ +See also INCOMPATIBLE OPTIONS below. --committer-date-is-author-date:: --ignore-date:: These flags are passed to 'git am' to easily change the dates of the rebased commits (see linkgit:git-am[1]). - Incompatible with the --interactive option. ++ +See also INCOMPATIBLE OPTIONS below. --signoff:: Add a Signed-off-by: trailer to all the rebased commits. Note that if `--interactive` is given then only commits marked to be - picked, edited or reworded will have the trailer added. Incompatible - with the `--preserve-merges` option. + picked, edited or reworded will have the trailer added. ++ +See also INCOMPATIBLE OPTIONS below. -i:: --interactive:: @@ -378,6 +395,8 @@ default is `--no-fork-point`, otherwise the default is `--fork-point`. The commit list format can be changed by setting the configuration option rebase.instructionFormat. A customized instruction format will automatically have the long commit hash prepended to the format. ++ +See also INCOMPATIBLE OPTIONS below. -r:: --rebase-merges[=(rebase-cousins|no-rebase-cousins)]:: @@ -404,7 +423,7 @@ It is currently only possible to recreate the merge commits using the `recursive` merge strategy; Different merge strategies can be used only via explicit `exec git merge -s <strategy> [...]` commands. + -See also REBASING MERGES below. +See also REBASING MERGES and INCOMPATIBLE OPTIONS below. -p:: --preserve-merges:: @@ -415,6 +434,8 @@ See also REBASING MERGES below. This uses the `--interactive` machinery internally, but combining it with the `--interactive` option explicitly is generally not a good idea unless you know what you are doing (see BUGS below). ++ +See also INCOMPATIBLE OPTIONS below. -x <cmd>:: --exec <cmd>:: @@ -437,6 +458,8 @@ squash/fixup series. + This uses the `--interactive` machinery internally, but it can be run without an explicit `--interactive`. ++ +See also INCOMPATIBLE OPTIONS below. --root:: Rebase all commits reachable from <branch>, instead of @@ -447,6 +470,8 @@ without an explicit `--interactive`. When used together with both --onto and --preserve-merges, 'all' root commits will be rewritten to have <newbase> as parent instead. ++ +See also INCOMPATIBLE OPTIONS below. --autosquash:: --no-autosquash:: @@ -461,11 +486,11 @@ without an explicit `--interactive`. too. The recommended way to create fixup/squash commits is by using the `--fixup`/`--squash` options of linkgit:git-commit[1]. + -This option is only valid when the `--interactive` option is used. -+ If the `--autosquash` option is enabled by default using the configuration variable `rebase.autoSquash`, this option can be used to override and disable this setting. ++ +See also INCOMPATIBLE OPTIONS below. --autostash:: --no-autostash:: @@ -475,17 +500,73 @@ used to override and disable this setting. with care: the final stash application after a successful rebase might result in non-trivial conflicts. ---no-ff:: - With --interactive, cherry-pick all rebased commits instead of - fast-forwarding over the unchanged ones. This ensures that the - entire history of the rebased branch is composed of new commits. -+ -Without --interactive, this is a synonym for --force-rebase. -+ -You may find this helpful after reverting a topic branch merge, as this option -recreates the topic branch with fresh commits so it can be remerged -successfully without needing to "revert the reversion" (see the -link:howto/revert-a-faulty-merge.html[revert-a-faulty-merge How-To] for details). +INCOMPATIBLE OPTIONS +-------------------- + +git-rebase has many flags that are incompatible with each other, +predominantly due to the fact that it has three different underlying +implementations: + + * one based on linkgit:git-am[1] (the default) + * one based on git-merge-recursive (merge backend) + * one based on linkgit:git-cherry-pick[1] (interactive backend) + +Flags only understood by the am backend: + + * --committer-date-is-author-date + * --ignore-date + * --whitespace + * --ignore-whitespace + * -C + +Flags understood by both merge and interactive backends: + + * --merge + * --strategy + * --strategy-option + * --allow-empty-message + +Flags only understood by the interactive backend: + + * --[no-]autosquash + * --rebase-merges + * --preserve-merges + * --interactive + * --exec + * --keep-empty + * --autosquash + * --edit-todo + * --root when used in combination with --onto + +Other incompatible flag pairs: + + * --preserve-merges and --interactive + * --preserve-merges and --signoff + * --preserve-merges and --rebase-merges + * --rebase-merges and --strategy + * --rebase-merges and --strategy-option + +BEHAVIORAL DIFFERENCES +----------------------- + + * empty commits: + + am-based rebase will drop any "empty" commits, whether the + commit started empty (had no changes relative to its parent to + start with) or ended empty (all changes were already applied + upstream in other commits). + + merge-based rebase does the same. + + interactive-based rebase will by default drop commits that + started empty and halt if it hits a commit that ended up empty. + The `--keep-empty` option exists for interactive rebases to allow + it to keep commits that started empty. + + * directory rename detection: + + merge-based and interactive-based rebases work fine with + directory rename detection. am-based rebases sometimes do not. include::merge-strategies.txt[] diff --git a/Documentation/technical/directory-rename-detection.txt b/Documentation/technical/directory-rename-detection.txt new file mode 100644 index 0000000000..1c0086e287 --- /dev/null +++ b/Documentation/technical/directory-rename-detection.txt @@ -0,0 +1,115 @@ +Directory rename detection +========================== + +Rename detection logic in diffcore-rename that checks for renames of +individual files is aggregated and analyzed in merge-recursive for cases +where combinations of renames indicate that a full directory has been +renamed. + +Scope of abilities +------------------ + +It is perhaps easiest to start with an example: + + * When all of x/a, x/b and x/c have moved to z/a, z/b and z/c, it is + likely that x/d added in the meantime would also want to move to z/d by + taking the hint that the entire directory 'x' moved to 'z'. + +More interesting possibilities exist, though, such as: + + * one side of history renames x -> z, and the other renames some file to + x/e, causing the need for the merge to do a transitive rename. + + * one side of history renames x -> z, but also renames all files within + x. For example, x/a -> z/alpha, x/b -> z/bravo, etc. + + * both 'x' and 'y' being merged into a single directory 'z', with a + directory rename being detected for both x->z and y->z. + + * not all files in a directory being renamed to the same location; + i.e. perhaps most the files in 'x' are now found under 'z', but a few + are found under 'w'. + + * a directory being renamed, which also contained a subdirectory that was + renamed to some entirely different location. (And perhaps the inner + directory itself contained inner directories that were renamed to yet + other locations). + + * combinations of the above; see t/t6043-merge-rename-directories.sh for + various interesting cases. + +Limitations -- applicability of directory renames +------------------------------------------------- + +In order to prevent edge and corner cases resulting in either conflicts +that cannot be represented in the index or which might be too complex for +users to try to understand and resolve, a couple basic rules limit when +directory rename detection applies: + + 1) If a given directory still exists on both sides of a merge, we do + not consider it to have been renamed. + + 2) If a subset of to-be-renamed files have a file or directory in the + way (or would be in the way of each other), "turn off" the directory + rename for those specific sub-paths and report the conflict to the + user. + + 3) If the other side of history did a directory rename to a path that + your side of history renamed away, then ignore that particular + rename from the other side of history for any implicit directory + renames (but warn the user). + +Limitations -- detailed rules and testcases +------------------------------------------- + +t/t6043-merge-rename-directories.sh contains extensive tests and commentary +which generate and explore the rules listed above. It also lists a few +additional rules: + + a) If renames split a directory into two or more others, the directory + with the most renames, "wins". + + b) Avoid directory-rename-detection for a path, if that path is the + source of a rename on either side of a merge. + + c) Only apply implicit directory renames to directories if the other side + of history is the one doing the renaming. + +Limitations -- support in different commands +-------------------------------------------- + +Directory rename detection is supported by 'merge' and 'cherry-pick'. +Other git commands which users might be surprised to see limited or no +directory rename detection support in: + + * diff + + Folks have requested in the past that `git diff` detect directory + renames and somehow simplify its output. It is not clear whether this + would be desirable or how the output should be simplified, so this was + simply not implemented. Further, to implement this, directory rename + detection logic would need to move from merge-recursive to + diffcore-rename. + + * am + + git-am tries to avoid a full three way merge, instead calling + git-apply. That prevents us from detecting renames at all, which may + defeat the directory rename detection. There is a fallback, though; if + the initial git-apply fails and the user has specified the -3 option, + git-am will fall back to a three way merge. However, git-am lacks the + necessary information to do a "real" three way merge. Instead, it has + to use build_fake_ancestor() to get a merge base that is missing files + whose rename may have been important to detect for directory rename + detection to function. + + * rebase + + Since am-based rebases work by first generating a bunch of patches + (which no longer record what the original commits were and thus don't + have the necessary info from which we can find a real merge-base), and + then calling git-am, this implies that am-based rebases will not always + successfully detect directory renames either (see the 'am' section + above). merged-based rebases (rebase -m) and cherry-pick-based rebases + (rebase -i) are not affected by this shortcoming, and fully support + directory rename detection. diff --git a/git-rebase.sh b/git-rebase.sh index f3b10c7f62..7973447645 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -20,23 +20,23 @@ onto=! rebase onto given branch instead of upstream r,rebase-merges? try to rebase merges instead of skipping them p,preserve-merges! try to recreate merges instead of ignoring them s,strategy=! use the given merge strategy +X,strategy-option=! pass the argument through to the merge strategy no-ff! cherry-pick all commits, even if unchanged +f,force-rebase! cherry-pick all commits, even if unchanged m,merge! use merging strategies to rebase i,interactive! let the user edit the list of commits to rebase x,exec=! add exec lines after each commit of the editable list k,keep-empty preserve empty commits during rebase allow-empty-message allow rebasing commits with empty messages -f,force-rebase! force rebase even if branch is up to date -X,strategy-option=! pass the argument through to the merge strategy stat! display a diffstat of what changed upstream n,no-stat! do not show diffstat of what changed upstream verify allow pre-rebase hook to run rerere-autoupdate allow rerere to update index with resolved conflicts root! rebase all reachable commits up to the root(s) autosquash move commits that begin with squash!/fixup! under -i +signoff add a Signed-off-by: line to each commit committer-date-is-author-date! passed to 'git am' ignore-date! passed to 'git am' -signoff passed to 'git am' whitespace=! passed to 'git apply' ignore-whitespace! passed to 'git apply' C=! passed to 'git apply' @@ -95,7 +95,7 @@ rebase_cousins= preserve_merges= autosquash= keep_empty= -allow_empty_message= +allow_empty_message=--allow-empty-message signoff= test "$(git config --bool rebase.autosquash)" = "true" && autosquash=t case "$(git config --bool commit.gpgsign)" in @@ -521,6 +521,24 @@ then git_format_patch_opt="$git_format_patch_opt --progress" fi +if test -n "$git_am_opt"; then + incompatible_opts=$(echo " $git_am_opt " | \ + sed -e 's/ -q / /g' -e 's/^ \(.*\) $/\1/') + if test -n "$interactive_rebase" + then + if test -n "$incompatible_opts" + then + die "$(gettext "error: cannot combine interactive options (--interactive, --exec, --rebase-merges, --preserve-merges, --keep-empty, --root + --onto) with am options ($incompatible_opts)")" + fi + fi + if test -n "$do_merge"; then + if test -n "$incompatible_opts" + then + die "$(gettext "error: cannot combine merge options (--merge, --strategy, --strategy-option) with am options ($incompatible_opts)")" + fi + fi +fi + if test -n "$signoff" then test -n "$preserve_merges" && @@ -529,6 +547,23 @@ then force_rebase=t fi +if test -n "$preserve_merges" +then + # Note: incompatibility with --signoff handled in signoff block above + # Note: incompatibility with --interactive is just a strong warning; + # git-rebase.txt caveats with "unless you know what you are doing" + test -n "$rebase_merges" && + die "$(gettext "error: cannot combine '--preserve_merges' with '--rebase-merges'")" +fi + +if test -n "$rebase_merges" +then + test -n "$strategy_opts" && + die "$(gettext "error: cannot combine '--rebase_merges' with '--strategy-option'")" + test -n "$strategy" && + die "$(gettext "error: cannot combine '--rebase_merges' with '--strategy'")" +fi + if test -z "$rebase_root" then case "$#" in diff --git a/t/t3401-rebase-and-am-rename.sh b/t/t3401-rebase-and-am-rename.sh new file mode 100755 index 0000000000..8f832957fc --- /dev/null +++ b/t/t3401-rebase-and-am-rename.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +test_description='git rebase + directory rename tests' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success 'setup testcase' ' + test_create_repo dir-rename && + ( + cd dir-rename && + + mkdir x && + test_seq 1 10 >x/a && + test_seq 11 20 >x/b && + test_seq 21 30 >x/c && + test_write_lines a b c d e f g h i >l && + git add x l && + git commit -m "Initial" && + + git branch O && + git branch A && + git branch B && + + git checkout A && + git mv x y && + git mv l letters && + git commit -m "Rename x to y, l to letters" && + + git checkout B && + echo j >>l && + test_seq 31 40 >x/d && + git add l x/d && + git commit -m "Modify l, add x/d" + ) +' + +test_expect_success 'rebase --interactive: directory rename detected' ' + ( + cd dir-rename && + + git checkout B^0 && + + set_fake_editor && + FAKE_LINES="1" git rebase --interactive A && + + git ls-files -s >out && + test_line_count = 5 out && + + test_path_is_file y/d && + test_path_is_missing x/d + ) +' + +test_expect_failure 'rebase (am): directory rename detected' ' + ( + cd dir-rename && + + git checkout B^0 && + + git rebase A && + + git ls-files -s >out && + test_line_count = 5 out && + + test_path_is_file y/d && + test_path_is_missing x/d + ) +' + +test_expect_success 'rebase --merge: directory rename detected' ' + ( + cd dir-rename && + + git checkout B^0 && + + git rebase --merge A && + + git ls-files -s >out && + test_line_count = 5 out && + + test_path_is_file y/d && + test_path_is_missing x/d + ) +' + +test_expect_failure 'am: directory rename detected' ' + ( + cd dir-rename && + + git checkout A^0 && + + git format-patch -1 B && + + git am --3way 0001*.patch && + + git ls-files -s >out && + test_line_count = 5 out && + + test_path_is_file y/d && + test_path_is_missing x/d + ) +' + +test_done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index d392160ba9..c5d39e2b23 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -553,15 +553,16 @@ test_expect_success '--continue tries to commit, even for "edit"' ' ' test_expect_success 'aborted --continue does not squash commits after "edit"' ' + test_when_finished "git rebase --abort" && old=$(git rev-parse HEAD) && test_tick && set_fake_editor && FAKE_LINES="edit 1" git rebase -i HEAD^ && echo "edited again" > file7 && git add file7 && - test_must_fail env FAKE_COMMIT_MESSAGE=" " git rebase --continue && - test $old = $(git rev-parse HEAD) && - git rebase --abort + echo all the things >>conflict && + test_must_fail git rebase --continue && + test $old = $(git rev-parse HEAD) ' test_expect_success 'auto-amend only edited commits after "edit"' ' diff --git a/t/t3405-rebase-malformed.sh b/t/t3405-rebase-malformed.sh index cb7c6de84a..da94dddc86 100755 --- a/t/t3405-rebase-malformed.sh +++ b/t/t3405-rebase-malformed.sh @@ -77,19 +77,14 @@ test_expect_success 'rebase commit with diff in message' ' ' test_expect_success 'rebase -m commit with empty message' ' - test_must_fail git rebase -m master empty-message-merge && - git rebase --abort && - git rebase -m --allow-empty-message master empty-message-merge + git rebase -m master empty-message-merge ' test_expect_success 'rebase -i commit with empty message' ' git checkout diff-in-message && set_fake_editor && - test_must_fail env FAKE_COMMIT_MESSAGE=" " FAKE_LINES="reword 1" \ - git rebase -i HEAD^ && - git rebase --abort && - FAKE_COMMIT_MESSAGE=" " FAKE_LINES="reword 1" \ - git rebase -i --allow-empty-message HEAD^ + env FAKE_COMMIT_MESSAGE=" " FAKE_LINES="reword 1" \ + git rebase -i HEAD^ ' test_done diff --git a/t/t3422-rebase-incompatible-options.sh b/t/t3422-rebase-incompatible-options.sh new file mode 100755 index 0000000000..bb78a6ec86 --- /dev/null +++ b/t/t3422-rebase-incompatible-options.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +test_description='test if rebase detects and aborts on incompatible options' +. ./test-lib.sh + +test_expect_success 'setup' ' + test_seq 2 9 >foo && + git add foo && + git commit -m orig && + + git branch A && + git branch B && + + git checkout A && + test_seq 1 9 >foo && + git add foo && + git commit -m A && + + git checkout B && + echo "q qfoo();" | q_to_tab >>foo && + git add foo && + git commit -m B +' + +# +# Rebase has lots of useful options like --whitepsace=fix, which are +# actually all built in terms of flags to git-am. Since neither +# --merge nor --interactive (nor any options that imply those two) use +# git-am, using them together will result in flags like --whitespace=fix +# being ignored. Make sure rebase warns the user and aborts instead. +# + +test_rebase_am_only () { + opt=$1 + shift + test_expect_success "$opt incompatible with --merge" " + git checkout B^0 && + test_must_fail git rebase $opt --merge A + " + + test_expect_success "$opt incompatible with --strategy=ours" " + git checkout B^0 && + test_must_fail git rebase $opt --strategy=ours A + " + + test_expect_success "$opt incompatible with --strategy-option=ours" " + git checkout B^0 && + test_must_fail git rebase $opt --strategy-option=ours A + " + + test_expect_success "$opt incompatible with --interactive" " + git checkout B^0 && + test_must_fail git rebase $opt --interactive A + " + + test_expect_success "$opt incompatible with --exec" " + git checkout B^0 && + test_must_fail git rebase $opt --exec 'true' A + " + +} + +test_rebase_am_only --whitespace=fix +test_rebase_am_only --ignore-whitespace +test_rebase_am_only --committer-date-is-author-date +test_rebase_am_only -C4 + +test_expect_success '--preserve-merges incompatible with --signoff' ' + git checkout B^0 && + test_must_fail git rebase --preserve-merges --signoff A +' + +test_expect_success '--preserve-merges incompatible with --rebase-merges' ' + git checkout B^0 && + test_must_fail git rebase --preserve-merges --rebase-merges A +' + +test_expect_success '--rebase-merges incompatible with --strategy' ' + git checkout B^0 && + test_must_fail git rebase --rebase-merges -s resolve A +' + +test_expect_success '--rebase-merges incompatible with --strategy-option' ' + git checkout B^0 && + test_must_fail git rebase --rebase-merges -Xignore-space-change A +' + +test_done |