git-commit-vandalism/t/t6012-rev-list-simplify.sh

278 lines
6.5 KiB
Bash
Raw Normal View History

#!/bin/sh
test_description='merge simplification'
. ./test-lib.sh
note () {
git tag "$1"
}
unnote () {
git name-rev --tags --stdin | sed -e "s|$OID_REGEX (tags/\([^)]*\)) |\1 |g"
}
#
# Create a test repo with interesting commit graph:
#
# A--B----------G--H--I--K--L
# \ \ / /
# \ \ / /
# C------E---F J
# \_/
#
# The commits are laid out from left-to-right starting with
# the root commit A and terminating at the tip commit L.
#
# There are a few places where we adjust the commit date or
# author date to make the --topo-order, --date-order, and
# --author-date-order flags produce different output.
test_expect_success setup '
echo "Hi there" >file &&
echo "initial" >lost &&
git add file lost &&
test_tick && git commit -m "Initial file and lost" &&
note A &&
git branch other-branch &&
git symbolic-ref HEAD refs/heads/unrelated &&
git rm -f "*" &&
echo "Unrelated branch" >side &&
git add side &&
test_tick && git commit -m "Side root" &&
note J &&
git checkout master &&
echo "Hello" >file &&
echo "second" >lost &&
git add file lost &&
test_tick && GIT_AUTHOR_DATE=$(($test_tick + 120)) git commit -m "Modified file and lost" &&
note B &&
git checkout other-branch &&
echo "Hello" >file &&
>lost &&
git add file lost &&
test_tick && git commit -m "Modified the file identically" &&
note C &&
echo "This is a stupid example" >another-file &&
git add another-file &&
test_tick && git commit -m "Add another file" &&
note D &&
test_tick &&
test_must_fail git merge -m "merge" master &&
>lost && git commit -a -m "merge" &&
note E &&
echo "Yet another" >elif &&
git add elif &&
test_tick && git commit -m "Irrelevant change" &&
note F &&
git checkout master &&
echo "Yet another" >elif &&
git add elif &&
test_tick && git commit -m "Another irrelevant change" &&
note G &&
test_tick && git merge -m "merge" other-branch &&
note H &&
echo "Final change" >file &&
test_tick && git commit -a -m "Final change" &&
note I &&
git checkout master &&
merge: refuse to create too cool a merge by default While it makes sense to allow merging unrelated histories of two projects that started independently into one, in the way "gitk" was merged to "git" itself aka "the coolest merge ever", such a merge is still an unusual event. Worse, if somebody creates an independent history by starting from a tarball of an established project and sends a pull request to the original project, "git merge" however happily creates such a merge without any sign of something unusual is happening. Teach "git merge" to refuse to create such a merge by default, unless the user passes a new "--allow-unrelated-histories" option to tell it that the user is aware that two unrelated projects are merged. Because such a "two project merge" is a rare event, a configuration option to always allow such a merge is not added. We could add the same option to "git pull" and have it passed through to underlying "git merge". I do not have a fundamental opposition against such a feature, but this commit does not do so and instead leaves it as low-hanging fruit for others, because such a "two project merge" would be done after fetching the other project into some location in the working tree of an existing project and making sure how well they fit together, it is sufficient to allow a local merge without such an option pass-through from "git pull" to "git merge". Many tests that are updated by this patch does the pass-through manually by turning: git pull something into its equivalent: git fetch something && git merge --allow-unrelated-histories FETCH_HEAD If somebody is inclined to add such an option, updated tests in this change need to be adjusted back to: git pull --allow-unrelated-histories something Signed-off-by: Junio C Hamano <gitster@pobox.com>
2016-03-18 21:21:09 +01:00
test_tick && git merge --allow-unrelated-histories -m "Coolest" unrelated &&
note K &&
echo "Immaterial" >elif &&
git add elif &&
test_tick && git commit -m "Last" &&
note L
'
FMT='tformat:%P %H | %s'
check_outcome () {
outcome=$1
shift
for c in $1
do
echo "$c"
done >expect &&
shift &&
param="$*" &&
test_expect_$outcome "log $param" '
git log --pretty="$FMT" --parents $param |
unnote >actual &&
sed -e "s/^.* \([^ ]*\) .*/\1/" >check <actual &&
test_cmp expect check
'
}
check_result () {
check_outcome success "$@"
}
check_result 'L K J I H F E D C G B A' --full-history --topo-order
check_result 'L K I H G F E D C B J A' --full-history
check_result 'L K I H G F E D C B J A' --full-history --date-order
check_result 'L K I H G F E D B C J A' --full-history --author-date-order
check_result 'K I H E C B A' --full-history -- file
check_result 'K I H E C B A' --full-history --topo-order -- file
check_result 'K I H E C B A' --full-history --date-order -- file
check_result 'K I H E B C A' --full-history --author-date-order -- file
check_result 'I E C B A' --simplify-merges -- file
check_result 'I E C B A' --simplify-merges --topo-order -- file
check_result 'I E C B A' --simplify-merges --date-order -- file
check_result 'I E B C A' --simplify-merges --author-date-order -- file
check_result 'I B A' -- file
check_result 'I B A' --topo-order -- file
check_result 'I B A' --date-order -- file
check_result 'I B A' --author-date-order -- file
Making pathspec limited log play nicer with --first-parent In a topic branch workflow, you often want to find the latest commit that merged a side branch that touched a particular area of the system, so that a new topic branch to work on that area can be forked from that commit. For example, I wanted to find an appropriate fork-point to queue Luke's changes related to git-p4 in contrib/fast-import/. "git log --first-parent" traverses the first-parent chain, and "-m --stat" shows the list of paths touched by commits including merge commits. We could ask the question this way: # What is the latest commit that touched that path? $ git log --first-parent --oneline -m --stat master | sed -e '/^ contrib\/fast-import\/git-p4 /q' | tail The above finds that 8cbfc11 (Merge branch 'pw/p4-view-updates', 2012-01-06) was such a commit. But a more natural way to spell this question is this: $ git log --first-parent --oneline -m --stat -1 master -- \ contrib/fast-import/git-p4 Unfortunately, this does not work. It finds ecb7cf9 (git-p4: rewrite view handling, 2012-01-02). This commit is a part of the merged topic branch and is _not_ on the first-parent path from the 'master': $ git show-branch 8cbfc11 ecb7cf9 ! [8cbfc11] Merge branch 'pw/p4-view-updates' ! [ecb7cf9] git-p4: rewrite view handling -- - [8cbfc11] Merge branch 'pw/p4-view-updates' + [8cbfc11^2] git-p4: view spec documentation ++ [ecb7cf9] git-p4: rewrite view handling The problem is caused by the merge simplification logic when it inspects the merge commit 8cbfc11. In this case, the history leading to the tip of 'master' did not touch git-p4 since 'pw/p4-view-updates' topic forked, and the result of the merge is simply a copy from the tip of the topic branch in the view limited by the given pathspec. The merge simplification logic discards the history on the mainline side of the merge, and pretends as if the sole parent of the merge is its second parent, i.e. the tip of the topic. While this simplification is correct in the general case, it is at least surprising if not outright wrong when the user explicitly asked to show the first-parent history. Here is an attempt to fix this issue, by not allowing us to compare the merge result with anything but the first parent when --first-parent is in effect, to avoid the history traversal veering off to the side branch. Signed-off-by: Junio C Hamano <gitster@pobox.com>
2012-01-19 20:58:45 +01:00
check_result 'H' --first-parent -- another-file
check_result 'H' --first-parent --topo-order -- another-file
check_result 'E C B A' --full-history E -- lost
test_expect_success 'full history simplification without parent' '
printf "%s\n" E C B A >expect &&
git log --pretty="$FMT" --full-history E -- lost |
unnote >actual &&
sed -e "s/^.* \([^ ]*\) .*/\1/" >check <actual &&
test_cmp expect check
'
log: use true parents for diff even when rewriting When using pathspec filtering in combination with diff-based log output, parent simplification happens before the diff is computed. The diff is therefore against the *simplified* parents. This works okay, arguably by accident, in the normal case: simplification reduces to one parent as long as the commit is TREESAME to it. So the simplified parent of any given commit must have the same tree contents on the filtered paths as its true (unfiltered) parent. However, --full-diff breaks this guarantee, and indeed gives pretty spectacular results when comparing the output of git log --graph --stat ... git log --graph --full-diff --stat ... (--graph internally kicks in parent simplification, much like --parents). To fix it, store a copy of the parent list before simplification (in a slab) whenever --full-diff is in effect. Then use the stored parents instead of the simplified ones in the commit display code paths. The latter do not actually check for --full-diff to avoid duplicated code; they just grab the original parents if save_parents() has not been called for this revision walk. For ordinary commits it should be obvious that this is the right thing to do. Merge commits are a bit subtle. Observe that with default simplification, merge simplification is an all-or-nothing decision: either the merge is TREESAME to one parent and disappears, or it is different from all parents and the parent list remains intact. Redundant parents are not pruned, so the existing code also shows them as a merge. So if we do show a merge commit, the parent list just consists of the rewrite result on each parent. Running, e.g., --cc on this in --full-diff mode is not very useful: if any commits were skipped, some hunks will disagree with all sides of the merge (with one side, because commits were skipped; with the others, because they didn't have those changes in the first place). This triggers --cc showing these hunks spuriously. Therefore I believe that even for merge commits it is better to show the diffs wrt. the original parents. Reported-by: Uwe Kleine-König <u.kleine-koenig@pengutronix.de> Helped-by: Junio C Hamano <gitster@pobox.com> Helped-by: Ramsay Jones <ramsay@ramsay1.demon.co.uk> Signed-off-by: Thomas Rast <trast@inf.ethz.ch> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2013-07-31 22:13:20 +02:00
test_expect_success '--full-diff is not affected by --parents' '
git log -p --pretty="%H" --full-diff -- file >expected &&
git log -p --pretty="%H" --full-diff --parents -- file >actual &&
test_cmp expected actual
'
revision: --show-pulls adds helpful merges The default file history simplification of "git log -- <path>" or "git rev-list -- <path>" focuses on providing the smallest set of commits that first contributed a change. The revision walk greatly restricts the set of walked commits by visiting only the first TREESAME parent of a merge commit, when one exists. This means that portions of the commit-graph are not walked, which can be a performance benefit, but can also "hide" commits that added changes but were ignored by a merge resolution. The --full-history option modifies this by walking all commits and reporting a merge commit as "interesting" if it has _any_ parent that is not TREESAME. This tends to be an over-representation of important commits, especially in an environment where most merge commits are created by pull request completion. Suppose we have a commit A and we create a commit B on top that changes our file. When we merge the pull request, we create a merge commit M. If no one else changed the file in the first-parent history between M and A, then M will not be TREESAME to its first parent, but will be TREESAME to B. Thus, the simplified history will be "B". However, M will appear in the --full-history mode. However, suppose that a number of topics T1, T2, ..., Tn were created based on commits C1, C2, ..., Cn between A and M as follows: A----C1----C2--- ... ---Cn----M------P1---P2--- ... ---Pn \ \ \ \ / / / / \ \__.. \ \/ ..__T1 / Tn \ \__.. /\ ..__T2 / \_____________________B \____________________/ If the commits T1, T2, ... Tn did not change the file, then all of P1 through Pn will be TREESAME to their first parent, but not TREESAME to their second. This means that all of those merge commits appear in the --full-history view, with edges that immediately collapse into the lower history without introducing interesting single-parent commits. The --simplify-merges option was introduced to remove these extra merge commits. By noticing that the rewritten parents are reachable from their first parents, those edges can be simplified away. Finally, the commits now look like single-parent commits that are TREESAME to their "only" parent. Thus, they are removed and this issue does not cause issues anymore. However, this also ends up removing the commit M from the history view! Even worse, the --simplify-merges option requires walking the entire history before returning a single result. Many Git users are using Git alongside a Git service that provides code storage alongside a code review tool commonly called "Pull Requests" or "Merge Requests" against a target branch. When these requests are accepted and merged, they typically create a merge commit whose first parent is the previous branch tip and the second parent is the tip of the topic branch used for the request. This presents a valuable order to the parents, but also makes that merge commit slightly special. Users may want to see not only which commits changed a file, but which pull requests merged those commits into their branch. In the previous example, this would mean the users want to see the merge commit "M" in addition to the single- parent commit "C". Users are even more likely to want these merge commits when they use pull requests to merge into a feature branch before merging that feature branch into their trunk. In some sense, users are asking for the "first" merge commit to bring in the change to their branch. As long as the parent order is consistent, this can be handled with the following rule: Include a merge commit if it is not TREESAME to its first parent, but is TREESAME to a later parent. These merges look like the merge commits that would result from running "git pull <topic>" on a main branch. Thus, the option to show these commits is called "--show-pulls". This has the added benefit of showing the commits created by closing a pull request or merge request on any of the Git hosting and code review platforms. To test these options, extend the standard test example to include a merge commit that is not TREESAME to its first parent. It is surprising that that option was not already in the example, as it is instructive. In particular, this extension demonstrates a common issue with file history simplification. When a user resolves a merge conflict using "-Xours" or otherwise ignoring one side of the conflict, they create a TREESAME edge that probably should not be TREESAME. This leads users to become frustrated and complain that "my change disappeared!" In my experience, showing them history with --full-history and --simplify-merges quickly reveals the problematic merge. As mentioned, this option is expensive to compute. The --show-pulls option _might_ show the merge commit (usually titled "resolving conflicts") more quickly. Of course, this depends on the user having the correct parent order, which is backwards when using "git pull master" from a topic branch. There are some special considerations when combining the --show-pulls option with --simplify-merges. This requires adding a new PULL_MERGE object flag to store the information from the initial TREESAME comparisons. This helps avoid dropping those commits in later filters. This is covered by a test, including how the parents can be simplified. Since "struct object" has already ruined its 32-bit alignment by using 33 bits across parsed, type, and flags member, let's not make it worse. PULL_MERGE is used in revision.c with the same value (1u<<15) as REACHABLE in commit-graph.c. The REACHABLE flag is only used when writing a commit-graph file, and a revision walk using --show-pulls does not happen in the same process. Care must be taken in the future to ensure this remains the case. Update Documentation/rev-list-options.txt with significant details around this option. This requires updating the example in the History Simplification section to demonstrate some of the problems with TREESAME second parents. Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2020-04-10 14:19:43 +02:00
#
# Create a new history to demonstrate the value of --show-pulls
# with respect to the subtleties of simplified history, --full-history,
# and --simplify-merges.
#
# .-A---M-----C--N---O---P
# / / \ \ \/ / /
# I B \ R-'`-Z' /
# \ / \/ /
# \ / /\ /
# `---X--' `---Y--'
#
# This example is explained in Documentation/rev-list-options.txt
test_expect_success 'rebuild repo' '
rm -rf .git * &&
git init &&
git switch -c main &&
echo base >file &&
git add file &&
test_commit I &&
echo A >file &&
git add file &&
test_commit A &&
git switch -c branchB I &&
echo B >file &&
git add file &&
test_commit B &&
git switch main &&
test_must_fail git merge -m "M" B &&
echo A >file &&
echo B >>file &&
git add file &&
git merge --continue &&
note M &&
echo C >other &&
git add other &&
test_commit C &&
git switch -c branchX I &&
echo X >file &&
git add file &&
test_commit X &&
git switch -c branchR M &&
git merge -m R -Xtheirs X &&
note R &&
git switch main &&
git merge -m N R &&
note N &&
git switch -c branchY M &&
echo Y >y &&
git add y &&
test_commit Y &&
git switch -c branchZ C &&
echo Z >z &&
git add z &&
test_commit Z &&
git switch main &&
git merge -m O Z &&
note O &&
git merge -m P Y &&
note P
'
check_result 'X I' -- file
check_result 'N R X I' --show-pulls -- file
check_result 'P O N R X M B A I' --full-history --topo-order -- file
check_result 'N R X M B A I' --simplify-merges --topo-order --show-pulls -- file
check_result 'R X M B A I' --simplify-merges --topo-order -- file
check_result 'N M A I' --first-parent -- file
check_result 'N M A I' --first-parent --show-pulls -- file
# --ancestry-path implies --full-history
check_result 'P O N R M' --topo-order \
--ancestry-path A..HEAD -- file
check_result 'P O N R M' --topo-order \
--show-pulls \
--ancestry-path A..HEAD -- file
check_result 'P O N R M' --topo-order \
--full-history \
--ancestry-path A..HEAD -- file
check_result 'R M' --topo-order \
--simplify-merges \
--ancestry-path A..HEAD -- file
check_result 'N R M' --topo-order \
--simplify-merges --show-pulls \
--ancestry-path A..HEAD -- file
test_expect_success 'log --graph --simplify-merges --show-pulls' '
cat >expect <<-\EOF &&
* N
* R
|\
| * X
* | M
|\ \
| * | B
| |/
* / A
|/
* I
EOF
git log --graph --pretty="%s" \
--simplify-merges --show-pulls \
-- file >actual &&
test_cmp expect actual
'
test_done