Skip to content

Commit 53d00b3

Browse files
Thomas Rastgitster
authored andcommitted
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 <[email protected]> Helped-by: Junio C Hamano <[email protected]> Helped-by: Ramsay Jones <[email protected]> Signed-off-by: Thomas Rast <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 0bde8c0 commit 53d00b3

File tree

7 files changed

+90
-3
lines changed

7 files changed

+90
-3
lines changed

combine-diff.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "refs.h"
1111
#include "userdiff.h"
1212
#include "sha1-array.h"
13+
#include "revision.h"
1314

1415
static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
1516
{
@@ -1383,7 +1384,7 @@ void diff_tree_combined(const unsigned char *sha1,
13831384
void diff_tree_combined_merge(const struct commit *commit, int dense,
13841385
struct rev_info *rev)
13851386
{
1386-
struct commit_list *parent = commit->parents;
1387+
struct commit_list *parent = get_saved_parents(rev, commit);
13871388
struct sha1_array parents = SHA1_ARRAY_INIT;
13881389

13891390
while (parent) {

commit.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,22 @@ unsigned commit_list_count(const struct commit_list *l)
377377
return c;
378378
}
379379

380+
struct commit_list *copy_commit_list(struct commit_list *list)
381+
{
382+
struct commit_list *head = NULL;
383+
struct commit_list **pp = &head;
384+
while (list) {
385+
struct commit_list *new;
386+
new = xmalloc(sizeof(struct commit_list));
387+
new->item = list->item;
388+
new->next = NULL;
389+
*pp = new;
390+
pp = &new->next;
391+
list = list->next;
392+
}
393+
return head;
394+
}
395+
380396
void free_commit_list(struct commit_list *list)
381397
{
382398
while (list) {

commit.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ struct commit_list *commit_list_insert_by_date(struct commit *item,
6262
struct commit_list **list);
6363
void commit_list_sort_by_date(struct commit_list **list);
6464

65+
/* Shallow copy of the input list */
66+
struct commit_list *copy_commit_list(struct commit_list *list);
67+
6568
void free_commit_list(struct commit_list *list);
6669

6770
/* Commit formats */

log-tree.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
738738
sha1 = commit->tree->object.sha1;
739739

740740
/* Root commit? */
741-
parents = commit->parents;
741+
parents = get_saved_parents(opt, commit);
742742
if (!parents) {
743743
if (opt->show_root_diff) {
744744
diff_root_tree_sha1(sha1, "", &opt->diffopt);

revision.c

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "string-list.h"
1616
#include "line-log.h"
1717
#include "mailmap.h"
18+
#include "commit-slab.h"
1819

1920
volatile show_early_output_fn_t show_early_output;
2021

@@ -2763,7 +2764,7 @@ static int commit_match(struct commit *commit, struct rev_info *opt)
27632764
return retval;
27642765
}
27652766

2766-
static inline int want_ancestry(struct rev_info *revs)
2767+
static inline int want_ancestry(const struct rev_info *revs)
27672768
{
27682769
return (revs->rewrite_parents || revs->children.name);
27692770
}
@@ -2820,6 +2821,14 @@ enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit)
28202821
if (action == commit_show &&
28212822
!revs->show_all &&
28222823
revs->prune && revs->dense && want_ancestry(revs)) {
2824+
/*
2825+
* --full-diff on simplified parents is no good: it
2826+
* will show spurious changes from the commits that
2827+
* were elided. So we save the parents on the side
2828+
* when --full-diff is in effect.
2829+
*/
2830+
if (revs->full_diff)
2831+
save_parents(revs, commit);
28232832
if (rewrite_parents(revs, commit, rewrite_one) < 0)
28242833
return commit_error;
28252834
}
@@ -3038,6 +3047,8 @@ struct commit *get_revision(struct rev_info *revs)
30383047
c = get_revision_internal(revs);
30393048
if (c && revs->graph)
30403049
graph_update(revs->graph, c);
3050+
if (!c)
3051+
free_saved_parents(revs);
30413052
return c;
30423053
}
30433054

@@ -3069,3 +3080,33 @@ void put_revision_mark(const struct rev_info *revs, const struct commit *commit)
30693080
fputs(mark, stdout);
30703081
putchar(' ');
30713082
}
3083+
3084+
define_commit_slab(saved_parents, struct commit_list *);
3085+
3086+
void save_parents(struct rev_info *revs, struct commit *commit)
3087+
{
3088+
struct commit_list **pp;
3089+
3090+
if (!revs->saved_parents_slab) {
3091+
revs->saved_parents_slab = xmalloc(sizeof(struct saved_parents));
3092+
init_saved_parents(revs->saved_parents_slab);
3093+
}
3094+
3095+
pp = saved_parents_at(revs->saved_parents_slab, commit);
3096+
assert(*pp == NULL);
3097+
*pp = copy_commit_list(commit->parents);
3098+
}
3099+
3100+
struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit)
3101+
{
3102+
if (!revs->saved_parents_slab)
3103+
return commit->parents;
3104+
3105+
return *saved_parents_at(revs->saved_parents_slab, commit);
3106+
}
3107+
3108+
void free_saved_parents(struct rev_info *revs)
3109+
{
3110+
if (revs->saved_parents_slab)
3111+
clear_saved_parents(revs->saved_parents_slab);
3112+
}

revision.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
struct rev_info;
2626
struct log_info;
2727
struct string_list;
28+
struct saved_parents;
2829

2930
struct rev_cmdline_info {
3031
unsigned int nr;
@@ -187,6 +188,9 @@ struct rev_info {
187188

188189
/* line level range that we are chasing */
189190
struct decoration line_log_data;
191+
192+
/* copies of the parent lists, for --full-diff display */
193+
struct saved_parents *saved_parents_slab;
190194
};
191195

192196
#define REV_TREE_SAME 0
@@ -273,4 +277,20 @@ typedef enum rewrite_result (*rewrite_parent_fn_t)(struct rev_info *revs, struct
273277

274278
extern int rewrite_parents(struct rev_info *revs, struct commit *commit,
275279
rewrite_parent_fn_t rewrite_parent);
280+
281+
/*
282+
* Save a copy of the parent list, and return the saved copy. This is
283+
* used by the log machinery to retrieve the original parents when
284+
* commit->parents has been modified by history simpification.
285+
*
286+
* You may only call save_parents() once per commit (this is checked
287+
* for non-root commits).
288+
*
289+
* get_saved_parents() will transparently return commit->parents if
290+
* history simplification is off.
291+
*/
292+
extern void save_parents(struct rev_info *revs, struct commit *commit);
293+
extern struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit);
294+
extern void free_saved_parents(struct rev_info *revs);
295+
276296
#endif

t/t6012-rev-list-simplify.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,10 @@ test_expect_success 'full history simplification without parent' '
127127
}
128128
'
129129

130+
test_expect_success '--full-diff is not affected by --parents' '
131+
git log -p --pretty="%H" --full-diff -- file >expected &&
132+
git log -p --pretty="%H" --full-diff --parents -- file >actual &&
133+
test_cmp expected actual
134+
'
135+
130136
test_done

0 commit comments

Comments
 (0)