git-rev-list: add --exclude-first-parent-only flag

It is useful to know when a branch first diverged in history from some integration branch in order to be able to enumerate the user's local changes. However, these local changes can include arbitrary merges, so it is necessary to ignore this merge structure when finding the divergence point. In order to do this, teach the "rev-list" family to accept "--exclude-first-parent-only", which restricts the traversal of excluded commits to only follow first parent links. -A-----E-F-G--main \ / / B-C-D--topic In this example, the goal is to return the set {B, C, D} which represents a topic branch that has been merged into main branch. `git rev-list topic ^main` will end up returning no commits since excluding main will end up traversing the commits on topic as well. `git rev-list --exclude-first-parent-only topic ^main` however will return {B, C, D} as desired. Add docs for the new flag, and clarify the doc for --first-parent to indicate that it applies to traversing the set of included commits only. Signed-off-by: Jerry Zhang <jerry@skydio.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jerry Zhang committed Jan 11, 2022 at 13:39 UTC 9d505b7b49c00c5fa99a25506e63a2ef326a0062
6 files changed +51 -26
Documentation/rev-list-options.txt
+15 -7
@@ -122,19 +122,27 @@ again. Equivalent forms are `--min-parents=0` (any commit has 0 or more
122 parents) and `--max-parents=-1` (negative numbers denote no upper limit).
123
124 --first-parent::
125 - Follow only the first parent commit upon seeing a merge
126 - commit. This option can give a better overview when
127 - viewing the evolution of a particular topic branch,
128 - because merges into a topic branch tend to be only about
129 - adjusting to updated upstream from time to time, and
130 - this option allows you to ignore the individual commits
131 - brought in to your history by such a merge.
125 + When finding commits to include, follow only the first
126 + parent commit upon seeing a merge commit. This option
127 + can give a better overview when viewing the evolution of
128 + a particular topic branch, because merges into a topic
129 + branch tend to be only about adjusting to updated upstream
130 + from time to time, and this option allows you to ignore
131 + the individual commits brought in to your history by such
132 + a merge.
133 ifdef::git-log[]
134 +
135 This option also changes default diff format for merge commits
136 to `first-parent`, see `--diff-merges=first-parent` for details.
137 endif::git-log[]
138
139 +--exclude-first-parent-only::
140 + When finding commits to exclude (with a '{caret}'), follow only
141 + the first parent commit upon seeing a merge commit.
142 + This can be used to find the set of changes in a topic branch
143 + from the point where it diverged from the remote branch, given
144 + that arbitrary merges can be valid topic branch changes.
145 +
146 --not::
147 Reverses the meaning of the '{caret}' prefix (or lack thereof)
148 for all following revision specifiers, up to the next `--not`.
blame.c
+1 -1
@@ -2615,7 +2615,7 @@ void assign_blame(struct blame_scoreboard *sb, int opt)
2615 else {
2616 commit->object.flags |= UNINTERESTING;
2617 if (commit->object.parsed)
2618 - mark_parents_uninteresting(commit);
2618 + mark_parents_uninteresting(sb->revs, commit);
2619 }
2620 /* treat root commit as boundary */
2621 if (!commit->parents && !sb->show_root)
revision.c
+20 -10
@@ -273,7 +273,7 @@ static void commit_stack_clear(struct commit_stack *stack)
273 stack->nr = stack->alloc = 0;
274 }
275
276 -static void mark_one_parent_uninteresting(struct commit *commit,
276 +static void mark_one_parent_uninteresting(struct rev_info *revs, struct commit *commit,
277 struct commit_stack *pending)
278 {
279 struct commit_list *l;
@@ -290,20 +290,26 @@ static void mark_one_parent_uninteresting(struct commit *commit,
290 * wasn't uninteresting), in which case we need
291 * to mark its parents recursively too..
292 */
293 - for (l = commit->parents; l; l = l->next)
293 + for (l = commit->parents; l; l = l->next) {
294 commit_stack_push(pending, l->item);
295 + if (revs && revs->exclude_first_parent_only)
296 + break;
297 + }
298 }
299
297 -void mark_parents_uninteresting(struct commit *commit)
300 +void mark_parents_uninteresting(struct rev_info *revs, struct commit *commit)
301 {
302 struct commit_stack pending = COMMIT_STACK_INIT;
303 struct commit_list *l;
304
302 - for (l = commit->parents; l; l = l->next)
303 - mark_one_parent_uninteresting(l->item, &pending);
305 + for (l = commit->parents; l; l = l->next) {
306 + mark_one_parent_uninteresting(revs, l->item, &pending);
307 + if (revs && revs->exclude_first_parent_only)
308 + break;
309 + }
310
311 while (pending.nr > 0)
306 - mark_one_parent_uninteresting(commit_stack_pop(&pending),
312 + mark_one_parent_uninteresting(revs, commit_stack_pop(&pending),
313 &pending);
314
315 commit_stack_clear(&pending);
@@ -441,7 +447,7 @@ static struct commit *handle_commit(struct rev_info *revs,
447 if (repo_parse_commit(revs->repo, commit) < 0)
448 die("unable to parse commit %s", name);
449 if (flags & UNINTERESTING) {
444 - mark_parents_uninteresting(commit);
450 + mark_parents_uninteresting(revs, commit);
451
452 if (!revs->topo_order || !generation_numbers_enabled(the_repository))
453 revs->limited = 1;
@@ -1124,7 +1130,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1130 if (repo_parse_commit_gently(revs->repo, p, 1) < 0)
1131 continue;
1132 if (p->parents)
1127 - mark_parents_uninteresting(p);
1133 + mark_parents_uninteresting(revs, p);
1134 if (p->object.flags & SEEN)
1135 continue;
1136 p->object.flags |= (SEEN | NOT_USER_GIVEN);
@@ -1132,6 +1138,8 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1138 commit_list_insert_by_date(p, list);
1139 if (queue)
1140 prio_queue_put(queue, p);
1141 + if (revs->exclude_first_parent_only)
1142 + break;
1143 }
1144 return 0;
1145 }
@@ -1422,7 +1430,7 @@ static int limit_list(struct rev_info *revs)
1430 if (process_parents(revs, commit, &original_list, NULL) < 0)
1431 return -1;
1432 if (obj->flags & UNINTERESTING) {
1425 - mark_parents_uninteresting(commit);
1433 + mark_parents_uninteresting(revs, commit);
1434 slop = still_interesting(original_list, date, slop, &interesting_cache);
1435 if (slop)
1436 continue;
@@ -2223,6 +2231,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2231 return argcount;
2232 } else if (!strcmp(arg, "--first-parent")) {
2233 revs->first_parent_only = 1;
2234 + } else if (!strcmp(arg, "--exclude-first-parent-only")) {
2235 + revs->exclude_first_parent_only = 1;
2236 } else if (!strcmp(arg, "--ancestry-path")) {
2237 revs->ancestry_path = 1;
2238 revs->simplify_history = 0;
@@ -3345,7 +3355,7 @@ static void explore_walk_step(struct rev_info *revs)
3355 return;
3356
3357 if (c->object.flags & UNINTERESTING)
3348 - mark_parents_uninteresting(c);
3358 + mark_parents_uninteresting(revs, c);
3359
3360 for (p = c->parents; p; p = p->next)
3361 test_flag_and_insert(&info->explore_queue, p->item, TOPO_WALK_EXPLORED);
revision.h
+2 -1
@@ -158,6 +158,7 @@ struct rev_info {
158 bisect:1,
159 ancestry_path:1,
160 first_parent_only:1,
161 + exclude_first_parent_only:1,
162 line_level_traverse:1,
163 tree_blobs_in_commit_order:1,
164
@@ -398,7 +399,7 @@ const char *get_revision_mark(const struct rev_info *revs,
399 void put_revision_mark(const struct rev_info *revs,
400 const struct commit *commit);
401
401 -void mark_parents_uninteresting(struct commit *commit);
402 +void mark_parents_uninteresting(struct rev_info *revs, struct commit *commit);
403 void mark_tree_uninteresting(struct repository *r, struct tree *tree);
404 void mark_trees_uninteresting_sparse(struct repository *r, struct oidset *trees);
405
shallow.c
+1 -1
@@ -603,7 +603,7 @@ static int mark_uninteresting(const char *refname, const struct object_id *oid,
603 if (!commit)
604 return 0;
605 commit->object.flags |= UNINTERESTING;
606 - mark_parents_uninteresting(commit);
606 + mark_parents_uninteresting(NULL, commit);
607 return 0;
608 }
609
t/t6012-rev-list-simplify.sh
+12 -6
@@ -16,13 +16,12 @@ unnote () {
16 }
17
18 #
19 -# Create a test repo with interesting commit graph:
19 +# Create a test repo with an interesting commit graph:
20 #
21 -# A--B----------G--H--I--K--L
22 -# \ \ / /
23 -# \ \ / /
24 -# C------E---F J
25 -# \_/
21 +# A-----B-----G--H--I--K--L
22 +# \ \ / /
23 +# \ \ / /
24 +# C--D--E--F J
25 #
26 # The commits are laid out from left-to-right starting with
27 # the root commit A and terminating at the tip commit L.
@@ -142,6 +141,13 @@ check_result 'I B A' --author-date-order -- file
141 check_result 'H' --first-parent -- another-file
142 check_result 'H' --first-parent --topo-order -- another-file
143
144 +check_result 'L K I H G B A' --first-parent L
145 +check_result 'F E D C' --exclude-first-parent-only F ^L
146 +check_result '' F ^L
147 +check_result 'L K I H G J' L ^F
148 +check_result 'L K I H G B J' --exclude-first-parent-only L ^F
149 +check_result 'L K I H G B' --exclude-first-parent-only --first-parent L ^F
150 +
151 check_result 'E C B A' --full-history E -- lost
152 test_expect_success 'full history simplification without parent' '
153 printf "%s\n" E C B A >expect &&