Git development
 help / color / mirror / Atom feed
From: Toon Claes <toon@iotcl.com>
To: Miklos Vajna <vmiklos@collabora.com>, gitster@pobox.com
Cc: szeder.dev@gmail.com, ps@pks.im, git@vger.kernel.org
Subject: Re: [PATCH v5 4/5] log: simplify --follow following renames for non-linear history
Date: Tue, 10 Nov 2026 17:45:50 +0100	[thread overview]
Message-ID: <877bik63ep.fsf@dev.null.iotcl.com.invalid> (raw)
In-Reply-To: <5ba93e49cff31e61621ac166f2d60b8133937f20.1794078554.git.vmiklos@collabora.com>

Miklos Vajna <vmiklos@collabora.com> writes:

> Code added in 304812ed33 (log: improve --follow following renames for
> non-linear history, 2026-06-22) implements the feature by introducing a
> new propagate_follow_pathspec_to_parent() function which adds a new call
> to diff_tree_oid(). Also it introduces a new loop to record what path
> each parent of a merge commit should use when following a path.
>
> This works, but it causes a performance regression: e.g. 'git log
> --stat --follow -- include/linux/rsc_table.h' in linux.git takes 6m43s,
> while it took 28s before that commit. The new code diffs each merge
> against each of its parents with rename detection, which is expensive
> when a parent doesn't have the followed path yet. Fixing this would
> require adding even more custom code, instead of just extending the
> existing code in log_tree_diff(). So first simplify the code here, and
> fix the performance regression in a follow-up commit.
>
> Remove propagate_follow_pathspec_to_parent() and the loop calling it
> for each parent. Instead, record the path to follow for each parent in
> the parent loop of log_tree_diff(), right after diff_tree_oid() updates
> the pathspec in case of a rename. Merges are not diffed by default, so
> with --follow, run the same loop for them as well, but without showing
> anything. With -c, --cc or --remerge-diff, show the merge diff after the
> loop. With --diff-merges=first-parent, keep diffing the other parents
> silently, unless the walk only follows first parents. This gets rid of
> the additional call to diff_tree_oid(). The rework has no positive
> performance impact yet, it just simplifies the code.
>
> The previous commit restores the path of the merge before diffing each
> parent, and also after the loop, for
> propagate_follow_pathspec_to_parent(). Keep restoring it between
> parents, but not after the loop, since there is no later propagation
> anymore.
>
> Signed-off-by: Miklos Vajna <vmiklos@collabora.com>
> ---
>  log-tree.c | 96 +++++++++++++++++++++++-------------------------------
>  1 file changed, 41 insertions(+), 55 deletions(-)
>
> diff --git a/log-tree.c b/log-tree.c
> index befc5c8b22..0aa27fb210 100644
> --- a/log-tree.c
> +++ b/log-tree.c
> @@ -1153,33 +1153,6 @@ void release_follow_pathspec_slab(struct rev_info *opt)
>  	FREE_AND_NULL(opt->follow_pathspec_slab);
>  }
>  
> -/* Compute a path to follow in parent, if there is one */
> -static void propagate_follow_pathspec_to_parent(struct rev_info *opt,
> -						struct commit *commit,
> -						struct commit *parent)
> -{
> -	struct diff_options diff_opts;
> -	const char *path;
> -
> -	parse_commit_or_die(parent);
> -	repo_diff_setup(opt->diffopt.repo, &diff_opts);
> -	copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec);
> -	diff_opts.flags.recursive = 1;
> -	diff_opts.flags.follow_renames = 1;
> -	diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
> -	diff_setup_done(&diff_opts);
> -	diff_tree_oid(get_commit_tree_oid(parent),
> -		      get_commit_tree_oid(commit),
> -		      "", &diff_opts);
> -
> -	path = pathspec_single_path(&diff_opts.pathspec);
> -	if (path)
> -		remember_follow_pathspec(opt, parent, path);
> -
> -	diff_queue_clear(&diff_queued_diff);
> -	diff_free(&diff_opts);
> -}
> -
>  /* Show the remerge or combined diff of a merge commit */
>  static int log_tree_merge_diff(struct rev_info *opt, struct commit *commit,
>  			       struct commit_list *parents,
> @@ -1208,10 +1181,11 @@ static int log_tree_merge_diff(struct rev_info *opt, struct commit *commit,
>  static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log_info *log)
>  {
>  	int showed_log;
> -	struct commit_list *parents;
> +	struct commit_list *parents, *all_parents;
>  	struct object_id *oid;
>  	int is_merge;
>  	int all_need_diff = opt->diff || opt->diffopt.flags.exit_with_status;
> +	int follow_only = 0;
>  	char *path = NULL;
>  
>  	if (!all_need_diff && !opt->merges_need_diff)
> @@ -1241,46 +1215,73 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
>  	}
>  
>  	if (is_merge) {
> -		if (opt->remerge_diff || opt->combine_merges)
> +		int merge_diff = opt->remerge_diff || opt->combine_merges;
> +
> +		if (merge_diff && !opt->diffopt.flags.follow_renames)
>  			return log_tree_merge_diff(opt, commit, parents, oid);
> -		if (opt->separate_merges) {
> +		if (opt->separate_merges && !merge_diff) {
>  			if (!opt->first_parent_merges) {
>  				/* Show parent info for multiple diffs */
>  				log->parent = parents->item;
>  			}
> +		} else if (opt->diffopt.flags.follow_renames) {
> +			/* Diff only to find the path to follow in parents */
> +			follow_only = 1;
>  		} else
>  			return 0;

This part of code has become pretty tough to read if you ask me. I've
tried to rewrite it:

        if (is_merge) {
                /* -c, --cc or --remerge-diff: show one diff for the whole merge. */
                int merge_diff = opt->remerge_diff || opt->combine_merges;

                /* Without --follow the parent loop is only needed for -m. */
                if (!opt->diffopt.flags.follow_renames) {
                        /* -c, --cc or --remerge-diff: show it, no parent loop. */
                        if (merge_diff)
                                return log_tree_merge_diff(opt, commit, parents, oid);
                        /* No -m either, i.e. merge diffs are off: show nothing. */
                        if (!opt->separate_merges)
                                return 0;
                }

                /* Run the loop silently, unless -m shows a diff per parent. */
                follow_only = !opt->separate_merges || merge_diff;

                /* -m showing all parents: label each diff "(from <parent>)". */
                if (!follow_only && !opt->first_parent_merges)
                        /* Label the first diff, others are labeled in parent loop. */
                        log->parent = parents->item;
        }

The comments I've added are possibly too verbose for the Git project,
but they helped me understand what happens at each line. I hope it's
functionally the same?

(This is as far as I have gotten in the review today, but I wanted to
send this out already)

>  	}
>  
> -	if (is_merge && opt->diffopt.flags.follow_renames)
> +	if (opt->diffopt.flags.follow_renames)
>  		path = xstrdup_or_null(
>  			pathspec_single_path(&opt->diffopt.pathspec));
>  
>  	showed_log = 0;
> +	all_parents = parents;
>  	for (;;) {
>  		struct commit *parent = parents->item;
>  
> -		/* Diff each parent with the path of the merge */
> -		if (path)
> -			set_pathspec_to_single_path(&opt->diffopt.pathspec,
> -						    path);
>  		parse_commit_or_die(parent);
>  		diff_tree_oid(get_commit_tree_oid(parent),
>  			      oid, "", &opt->diffopt);
> -		log_tree_diff_flush(opt);
> +		if (follow_only) {
> +			diff_queue_clear(&diff_queued_diff);
> +			opt->diffopt.found_follow = 0;
> +		} else {
> +			log_tree_diff_flush(opt);
> +		}
>  
>  		showed_log |= !opt->loginfo;
>  
> +		/* The diff updated the pathspec in case of a rename */
> +		if (path)
> +			remember_follow_pathspec(opt, parent,
> +				pathspec_single_path(&opt->diffopt.pathspec));
> +
>  		/* Set up the log info for the next parent, if any.. */
>  		parents = parents->next;
> -		if (!parents || opt->first_parent_merges)
> +		if (!parents)
>  			break;
> +		if (opt->first_parent_merges) {
> +			if (!path || opt->first_parent_only)
> +				break;
> +			/* Only find the path to follow in the other parents */
> +			follow_only = 1;
> +		}
> +		if (path)
> +			set_pathspec_to_single_path(&opt->diffopt.pathspec,
> +						    path);
> +		if (follow_only)
> +			continue;
>  		log->parent = parents->item;
>  		opt->loginfo = log;
>  	}
> -	/* Propagate to parents starting from the path of the merge, too */
> -	if (path)
> -		set_pathspec_to_single_path(&opt->diffopt.pathspec, path);
> +	if (follow_only && (opt->remerge_diff || opt->combine_merges)) {
> +		/* Show the merge diff with the path of this commit */
> +		if (path)
> +			set_pathspec_to_single_path(&opt->diffopt.pathspec,
> +						    path);
> +		showed_log = log_tree_merge_diff(opt, commit, all_parents, oid);
> +	}
>  	free(path);
>  	return showed_log;
>  }
> @@ -1319,21 +1320,6 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
>  		fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
>  	if (shown)
>  		show_diff_of_diff(opt);
> -
> -	/* Record what path each parent of this commit should use */
> -	if (opt->diffopt.flags.follow_renames) {
> -		struct commit_list *parents = get_saved_parents(opt, commit);
> -		if (parents && parents->next) {
> -			struct commit_list *p;
> -			for (p = parents; p; p = p->next)
> -				propagate_follow_pathspec_to_parent(opt, commit,
> -								    p->item);
> -		} else if (parents) {
> -			remember_follow_pathspec(opt, parents->item,
> -				pathspec_single_path(&opt->diffopt.pathspec));
> -		}
> -	}
> -
>  	opt->loginfo = NULL;
>  	maybe_flush_or_die(opt->diffopt.file, "stdout");
>  	opt->diffopt.no_free = no_free;
> -- 
> 2.51.0
>

-- 
Laters,
Toon

  reply	other threads:[~2026-11-10 16:45 UTC|newest]

Thread overview: 42+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-13 13:55 Performance regression in `git log --follow` Patrick Steinhardt
2026-10-13 21:24 ` SZEDER Gábor
2026-10-15 14:48   ` [PATCH] log: speed up --follow following renames for non-linear history Miklos Vajna
2026-10-15 16:57     ` Junio C Hamano
2026-10-19  6:10       ` [PATCH v2] " Miklos Vajna
2026-10-22 20:45         ` [PATCH v3 0/4] " Miklos Vajna
2026-10-22 20:45           ` [PATCH v3 1/4] t4219: test more --follow cases across merges Miklos Vajna
2026-10-22 20:46           ` [PATCH v3 2/4] log: extract showing the remerge/combined diff of a merge commit Miklos Vajna
2026-10-22 20:46           ` [PATCH v3 3/4] log: simplify --follow following renames for non-linear history Miklos Vajna
2026-10-26 11:40             ` Patrick Steinhardt
2026-10-22 20:47           ` [PATCH v3 4/4] log: speed up " Miklos Vajna
2026-10-26 11:40             ` Patrick Steinhardt
2026-10-26 11:42           ` [PATCH v3 0/4] " Patrick Steinhardt
2026-10-30 20:07           ` [PATCH v4 0/6] " Miklos Vajna
2026-10-30 20:07             ` [PATCH v4 1/6] t4219: test more --follow cases across merges Miklos Vajna
2026-10-30 20:07             ` [PATCH v4 2/6] log: extract showing the remerge/combined diff of a merge commit Miklos Vajna
2026-10-30 20:08             ` [PATCH v4 3/6] log: fix -m --follow losing the history of later parents Miklos Vajna
2026-10-30 20:08             ` [PATCH v4 4/6] log: simplify --follow following renames for non-linear history Miklos Vajna
2026-10-30 20:09             ` [PATCH v4 5/6] log: speed up " Miklos Vajna
2026-11-06 14:14               ` Patrick Steinhardt
2026-11-09  8:02                 ` Miklos Vajna
2026-10-30 20:10             ` [PATCH v4 6/6] log: fix --follow following an edit on a side the merge did not take Miklos Vajna
2026-11-06 14:14               ` Patrick Steinhardt
2026-11-09  8:03                 ` Miklos Vajna
2026-11-06 14:14             ` [PATCH v4 0/6] log: speed up --follow following renames for non-linear history Patrick Steinhardt
2026-11-09  8:01               ` Miklos Vajna
2026-11-09  8:03             ` [PATCH v5 0/5] " Miklos Vajna
2026-11-09  8:04               ` [PATCH v5 1/5] t4219: test more --follow cases across merges Miklos Vajna
2026-11-10 23:06                 ` Justin Tobler
2026-11-09  8:05               ` [PATCH v5 2/5] log: extract showing the remerge/combined diff of a merge commit Miklos Vajna
2026-11-09  8:05               ` [PATCH v5 3/5] log: fix -m --follow losing the history of later parents Miklos Vajna
2026-11-09  8:05               ` [PATCH v5 4/5] log: simplify --follow following renames for non-linear history Miklos Vajna
2026-11-10 16:45                 ` Toon Claes [this message]
2026-11-09  8:06               ` [PATCH v5 5/5] log: speed up " Miklos Vajna
2026-11-11  0:40               ` [PATCH v5 0/5] " Justin Tobler
2026-11-11 10:43                 ` Patrick Steinhardt
2026-11-11 12:41                   ` Miklos Vajna
2026-11-11 13:29                     ` Patrick Steinhardt
2026-11-12  9:05                       ` [PATCH] log: make --follow following renames for non-linear history optional Miklos Vajna
2026-11-11 16:44                 ` [PATCH v5 0/5] log: speed up --follow following renames for non-linear history Junio C Hamano
2026-10-16 20:37     ` [PATCH] " Junio C Hamano
2026-10-14  6:16 ` Performance regression in `git log --follow` Miklos Vajna

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=877bik63ep.fsf@dev.null.iotcl.com.invalid \
    --to=toon@iotcl.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=ps@pks.im \
    --cc=szeder.dev@gmail.com \
    --cc=vmiklos@collabora.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox