]> git.ipfire.org Git - thirdparty/git.git/commitdiff
log: use true parents for diff even when rewriting
authorThomas Rast <trast@inf.ethz.ch>
Wed, 31 Jul 2013 20:13:20 +0000 (22:13 +0200)
committerJunio C Hamano <gitster@pobox.com>
Thu, 1 Aug 2013 17:25:48 +0000 (10:25 -0700)
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>
combine-diff.c
commit.c
commit.h
log-tree.c
revision.c
revision.h
t/t6012-rev-list-simplify.sh

index 6dc06093d3c1eb6f3ab7a9cc79f0f192e16dc617..3d2aaf34ab53accccf1c3877557522ea012be42d 100644 (file)
@@ -10,6 +10,7 @@
 #include "refs.h"
 #include "userdiff.h"
 #include "sha1-array.h"
+#include "revision.h"
 
 static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
 {
@@ -1383,7 +1384,7 @@ void diff_tree_combined(const unsigned char *sha1,
 void diff_tree_combined_merge(const struct commit *commit, int dense,
                              struct rev_info *rev)
 {
-       struct commit_list *parent = commit->parents;
+       struct commit_list *parent = get_saved_parents(rev, commit);
        struct sha1_array parents = SHA1_ARRAY_INIT;
 
        while (parent) {
index e5862f6d7c813b4bcc5dd672383e56152ceb6f80..5ecdb38b3e7601419eed27ad81063ed6c8e5808e 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -377,6 +377,22 @@ unsigned commit_list_count(const struct commit_list *l)
        return c;
 }
 
+struct commit_list *copy_commit_list(struct commit_list *list)
+{
+       struct commit_list *head = NULL;
+       struct commit_list **pp = &head;
+       while (list) {
+               struct commit_list *new;
+               new = xmalloc(sizeof(struct commit_list));
+               new->item = list->item;
+               new->next = NULL;
+               *pp = new;
+               pp = &new->next;
+               list = list->next;
+       }
+       return head;
+}
+
 void free_commit_list(struct commit_list *list)
 {
        while (list) {
index 35cc4e266bd4afe0e00d2efb2781db2e95906936..ca766d2f4aa0df98d958e19449ab24d417c5539b 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -62,6 +62,9 @@ struct commit_list *commit_list_insert_by_date(struct commit *item,
                                    struct commit_list **list);
 void commit_list_sort_by_date(struct commit_list **list);
 
+/* Shallow copy of the input list */
+struct commit_list *copy_commit_list(struct commit_list *list);
+
 void free_commit_list(struct commit_list *list);
 
 /* Commit formats */
index a49d8e895d3ad24f00e8504ec3eccfbddead6b4c..8534d91826f3cf05f810a28728ee8bd51649d819 100644 (file)
@@ -738,7 +738,7 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
        sha1 = commit->tree->object.sha1;
 
        /* Root commit? */
-       parents = commit->parents;
+       parents = get_saved_parents(opt, commit);
        if (!parents) {
                if (opt->show_root_diff) {
                        diff_root_tree_sha1(sha1, "", &opt->diffopt);
index 84ccc0529b75508be5d1603a9e73790ca2b102fe..e3ca9361b460b8d7084ec8d26b325e6ba29af3f1 100644 (file)
@@ -15,6 +15,7 @@
 #include "string-list.h"
 #include "line-log.h"
 #include "mailmap.h"
+#include "commit-slab.h"
 
 volatile show_early_output_fn_t show_early_output;
 
@@ -2763,7 +2764,7 @@ static int commit_match(struct commit *commit, struct rev_info *opt)
        return retval;
 }
 
-static inline int want_ancestry(struct rev_info *revs)
+static inline int want_ancestry(const struct rev_info *revs)
 {
        return (revs->rewrite_parents || revs->children.name);
 }
@@ -2820,6 +2821,14 @@ enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit)
        if (action == commit_show &&
            !revs->show_all &&
            revs->prune && revs->dense && want_ancestry(revs)) {
+               /*
+                * --full-diff on simplified parents is no good: it
+                * will show spurious changes from the commits that
+                * were elided.  So we save the parents on the side
+                * when --full-diff is in effect.
+                */
+               if (revs->full_diff)
+                       save_parents(revs, commit);
                if (rewrite_parents(revs, commit, rewrite_one) < 0)
                        return commit_error;
        }
@@ -3038,6 +3047,8 @@ struct commit *get_revision(struct rev_info *revs)
        c = get_revision_internal(revs);
        if (c && revs->graph)
                graph_update(revs->graph, c);
+       if (!c)
+               free_saved_parents(revs);
        return c;
 }
 
@@ -3069,3 +3080,33 @@ void put_revision_mark(const struct rev_info *revs, const struct commit *commit)
        fputs(mark, stdout);
        putchar(' ');
 }
+
+define_commit_slab(saved_parents, struct commit_list *);
+
+void save_parents(struct rev_info *revs, struct commit *commit)
+{
+       struct commit_list **pp;
+
+       if (!revs->saved_parents_slab) {
+               revs->saved_parents_slab = xmalloc(sizeof(struct saved_parents));
+               init_saved_parents(revs->saved_parents_slab);
+       }
+
+       pp = saved_parents_at(revs->saved_parents_slab, commit);
+       assert(*pp == NULL);
+       *pp = copy_commit_list(commit->parents);
+}
+
+struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit)
+{
+       if (!revs->saved_parents_slab)
+               return commit->parents;
+
+       return *saved_parents_at(revs->saved_parents_slab, commit);
+}
+
+void free_saved_parents(struct rev_info *revs)
+{
+       if (revs->saved_parents_slab)
+               clear_saved_parents(revs->saved_parents_slab);
+}
index 95859ba119033ba17346c701493df1474eec5df2..e7f1d211bf0a203978a3024bcbae5c98f25c60cf 100644 (file)
@@ -25,6 +25,7 @@
 struct rev_info;
 struct log_info;
 struct string_list;
+struct saved_parents;
 
 struct rev_cmdline_info {
        unsigned int nr;
@@ -187,6 +188,9 @@ struct rev_info {
 
        /* line level range that we are chasing */
        struct decoration line_log_data;
+
+       /* copies of the parent lists, for --full-diff display */
+       struct saved_parents *saved_parents_slab;
 };
 
 #define REV_TREE_SAME          0
@@ -273,4 +277,20 @@ typedef enum rewrite_result (*rewrite_parent_fn_t)(struct rev_info *revs, struct
 
 extern int rewrite_parents(struct rev_info *revs, struct commit *commit,
        rewrite_parent_fn_t rewrite_parent);
+
+/*
+ * Save a copy of the parent list, and return the saved copy.  This is
+ * used by the log machinery to retrieve the original parents when
+ * commit->parents has been modified by history simpification.
+ *
+ * You may only call save_parents() once per commit (this is checked
+ * for non-root commits).
+ *
+ * get_saved_parents() will transparently return commit->parents if
+ * history simplification is off.
+ */
+extern void save_parents(struct rev_info *revs, struct commit *commit);
+extern struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit);
+extern void free_saved_parents(struct rev_info *revs);
+
 #endif
index 57ce2395d6738617797d0ba270874abd55a3b899..fde5e712eb512791c893de7b393fe36e382a3f6d 100755 (executable)
@@ -127,4 +127,10 @@ test_expect_success 'full history simplification without parent' '
        }
 '
 
+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
+'
+
 test_done