revision: allow --ancestry-path to take an argument

We have long allowed users to run e.g. git log --ancestry-path master..seen which shows all commits which satisfy all three of these criteria: * are an ancestor of seen * are not an ancestor of master * have master as an ancestor This commit allows another variant: git log --ancestry-path=$TOPIC master..seen which shows all commits which satisfy all of these criteria: * are an ancestor of seen * are not an ancestor of master * have $TOPIC in their ancestry-path that last bullet can be defined as commits meeting any of these criteria: * are an ancestor of $TOPIC * have $TOPIC as an ancestor * are $TOPIC This also allows multiple --ancestry-path arguments, which can be used to find commits with any of the given topics in their ancestry path. Signed-off-by: Elijah Newren <newren@gmail.com> Acked-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Aug 19, 2022 at 04:28 UTC 257418c59040c13bfa839e01922e21833cda6a52
5 files changed +115 -43
Documentation/rev-list-options.txt
+34 -10
@@ -389,12 +389,14 @@ Default mode::
389 merges from the resulting history, as there are no selected
390 commits contributing to this merge.
391
392 ---ancestry-path::
392 +--ancestry-path[=<commit>]::
393 When given a range of commits to display (e.g. 'commit1..commit2'
394 - or 'commit2 {caret}commit1'), only display commits that exist
395 - directly on the ancestry chain between the 'commit1' and
396 - 'commit2', i.e. commits that are both descendants of 'commit1',
397 - and ancestors of 'commit2'.
394 + or 'commit2 {caret}commit1'), only display commits in that range
395 + that are ancestors of <commit>, descendants of <commit>, or
396 + <commit> itself. If no commit is specified, use 'commit1' (the
397 + excluded part of the range) as <commit>. Can be passed multiple
398 + times; if so, a commit is included if it is any of the commits
399 + given or if it is an ancestor or descendant of one of them.
400
401 A more detailed explanation follows.
402
@@ -568,11 +570,10 @@ Note the major differences in `N`, `P`, and `Q` over `--full-history`:
570
571 There is another simplification mode available:
572
571 ---ancestry-path::
572 - Limit the displayed commits to those directly on the ancestry
573 - chain between the ``from'' and ``to'' commits in the given commit
574 - range. I.e. only display commits that are ancestor of the ``to''
575 - commit and descendants of the ``from'' commit.
573 +--ancestry-path[=<commit>]::
574 + Limit the displayed commits to those which are an ancestor of
575 + <commit>, or which are a descendant of <commit>, or are <commit>
576 + itself.
577 +
578 As an example use case, consider the following commit history:
579 +
@@ -604,6 +605,29 @@ option does. Applied to the 'D..M' range, it results in:
605 \
606 L--M
607 -----------------------------------------------------------------------
608 ++
609 +We can also use `--ancestry-path=D` instead of `--ancestry-path` which
610 +means the same thing when applied to the 'D..M' range but is just more
611 +explicit.
612 ++
613 +If we instead are interested in a given topic within this range, and all
614 +commits affected by that topic, we may only want to view the subset of
615 +`D..M` which contain that topic in their ancestry path. So, using
616 +`--ancestry-path=H D..M` for example would result in:
617 ++
618 +-----------------------------------------------------------------------
619 + E
620 + \
621 + G---H---I---J
622 + \
623 + L--M
624 +-----------------------------------------------------------------------
625 ++
626 +Whereas `--ancestry-path=K D..M` would result in
627 ++
628 +-----------------------------------------------------------------------
629 + K---------------L--M
630 +-----------------------------------------------------------------------
631
632 Before discussing another option, `--show-pulls`, we need to
633 create a new example history.
object.h
+1 -1
@@ -59,7 +59,7 @@ struct object_array {
59
60 /*
61 * object flag allocation:
62 - * revision.h: 0---------10 15 23------26
62 + * revision.h: 0---------10 15 23------27
63 * fetch-pack.c: 01 67
64 * negotiator/default.c: 2--5
65 * walker.c: 0-2
revision.c
+59 -30
@@ -1105,7 +1105,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1105 struct commit_list **list, struct prio_queue *queue)
1106 {
1107 struct commit_list *parent = commit->parents;
1108 - unsigned left_flag;
1108 + unsigned pass_flags;
1109
1110 if (commit->object.flags & ADDED)
1111 return 0;
@@ -1160,7 +1160,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1160 if (revs->no_walk)
1161 return 0;
1162
1163 - left_flag = (commit->object.flags & SYMMETRIC_LEFT);
1163 + pass_flags = (commit->object.flags & (SYMMETRIC_LEFT | ANCESTRY_PATH));
1164
1165 for (parent = commit->parents; parent; parent = parent->next) {
1166 struct commit *p = parent->item;
@@ -1181,7 +1181,7 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1181 if (!*slot)
1182 *slot = *revision_sources_at(revs->sources, commit);
1183 }
1184 - p->object.flags |= left_flag;
1184 + p->object.flags |= pass_flags;
1185 if (!(p->object.flags & SEEN)) {
1186 p->object.flags |= (SEEN | NOT_USER_GIVEN);
1187 if (list)
@@ -1304,13 +1304,24 @@ static int still_interesting(struct commit_list *src, timestamp_t date, int slop
1304 }
1305
1306 /*
1307 - * "rev-list --ancestry-path A..B" computes commits that are ancestors
1308 - * of B but not ancestors of A but further limits the result to those
1309 - * that are descendants of A. This takes the list of bottom commits and
1310 - * the result of "A..B" without --ancestry-path, and limits the latter
1311 - * further to the ones that can reach one of the commits in "bottom".
1307 + * "rev-list --ancestry-path=C_0 [--ancestry-path=C_1 ...] A..B"
1308 + * computes commits that are ancestors of B but not ancestors of A but
1309 + * further limits the result to those that have any of C in their
1310 + * ancestry path (i.e. are either ancestors of any of C, descendants
1311 + * of any of C, or are any of C). If --ancestry-path is specified with
1312 + * no commit, we use all bottom commits for C.
1313 + *
1314 + * Before this function is called, ancestors of C will have already
1315 + * been marked with ANCESTRY_PATH previously.
1316 + *
1317 + * This takes the list of bottom commits and the result of "A..B"
1318 + * without --ancestry-path, and limits the latter further to the ones
1319 + * that have any of C in their ancestry path. Since the ancestors of C
1320 + * have already been marked (a prerequisite of this function), we just
1321 + * need to mark the descendants, then exclude any commit that does not
1322 + * have any of these marks.
1323 */
1313 -static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *list)
1324 +static void limit_to_ancestry(struct commit_list *bottoms, struct commit_list *list)
1325 {
1326 struct commit_list *p;
1327 struct commit_list *rlist = NULL;
@@ -1323,7 +1334,7 @@ static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *li
1334 for (p = list; p; p = p->next)
1335 commit_list_insert(p->item, &rlist);
1336
1326 - for (p = bottom; p; p = p->next)
1337 + for (p = bottoms; p; p = p->next)
1338 p->item->object.flags |= TMP_MARK;
1339
1340 /*
@@ -1356,38 +1367,39 @@ static void limit_to_ancestry(struct commit_list *bottom, struct commit_list *li
1367 */
1368
1369 /*
1359 - * The ones that are not marked with TMP_MARK are uninteresting
1370 + * The ones that are not marked with either TMP_MARK or
1371 + * ANCESTRY_PATH are uninteresting
1372 */
1373 for (p = list; p; p = p->next) {
1374 struct commit *c = p->item;
1363 - if (c->object.flags & TMP_MARK)
1375 + if (c->object.flags & (TMP_MARK | ANCESTRY_PATH))
1376 continue;
1377 c->object.flags |= UNINTERESTING;
1378 }
1379
1368 - /* We are done with the TMP_MARK */
1380 + /* We are done with TMP_MARK and ANCESTRY_PATH */
1381 for (p = list; p; p = p->next)
1370 - p->item->object.flags &= ~TMP_MARK;
1371 - for (p = bottom; p; p = p->next)
1372 - p->item->object.flags &= ~TMP_MARK;
1382 + p->item->object.flags &= ~(TMP_MARK | ANCESTRY_PATH);
1383 + for (p = bottoms; p; p = p->next)
1384 + p->item->object.flags &= ~(TMP_MARK | ANCESTRY_PATH);
1385 free_commit_list(rlist);
1386 }
1387
1388 /*
1377 - * Before walking the history, keep the set of "negative" refs the
1378 - * caller has asked to exclude.
1389 + * Before walking the history, add the set of "negative" refs the
1390 + * caller has asked to exclude to the bottom list.
1391 *
1392 * This is used to compute "rev-list --ancestry-path A..B", as we need
1393 * to filter the result of "A..B" further to the ones that can actually
1394 * reach A.
1395 */
1384 -static struct commit_list *collect_bottom_commits(struct commit_list *list)
1396 +static void collect_bottom_commits(struct commit_list *list,
1397 + struct commit_list **bottom)
1398 {
1386 - struct commit_list *elem, *bottom = NULL;
1399 + struct commit_list *elem;
1400 for (elem = list; elem; elem = elem->next)
1401 if (elem->item->object.flags & BOTTOM)
1389 - commit_list_insert(elem->item, &bottom);
1390 - return bottom;
1402 + commit_list_insert(elem->item, bottom);
1403 }
1404
1405 /* Assumes either left_only or right_only is set */
@@ -1414,12 +1426,12 @@ static int limit_list(struct rev_info *revs)
1426 struct commit_list *original_list = revs->commits;
1427 struct commit_list *newlist = NULL;
1428 struct commit_list **p = &newlist;
1417 - struct commit_list *bottom = NULL;
1429 struct commit *interesting_cache = NULL;
1430
1420 - if (revs->ancestry_path) {
1421 - bottom = collect_bottom_commits(original_list);
1422 - if (!bottom)
1431 + if (revs->ancestry_path_implicit_bottoms) {
1432 + collect_bottom_commits(original_list,
1433 + &revs->ancestry_path_bottoms);
1434 + if (!revs->ancestry_path_bottoms)
1435 die("--ancestry-path given but there are no bottom commits");
1436 }
1437
@@ -1464,9 +1476,8 @@ static int limit_list(struct rev_info *revs)
1476 if (revs->left_only || revs->right_only)
1477 limit_left_right(newlist, revs);
1478
1467 - if (bottom)
1468 - limit_to_ancestry(bottom, newlist);
1469 - free_commit_list(bottom);
1479 + if (revs->ancestry_path)
1480 + limit_to_ancestry(revs->ancestry_path_bottoms, newlist);
1481
1482 /*
1483 * Check if any commits have become TREESAME by some of their parents
@@ -2213,7 +2224,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2224 const struct setup_revision_opt* opt)
2225 {
2226 const char *arg = argv[0];
2216 - const char *optarg;
2227 + const char *optarg = NULL;
2228 int argcount;
2229 const unsigned hexsz = the_hash_algo->hexsz;
2230
@@ -2284,6 +2295,23 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2295 revs->ancestry_path = 1;
2296 revs->simplify_history = 0;
2297 revs->limited = 1;
2298 + revs->ancestry_path_implicit_bottoms = 1;
2299 + } else if (skip_prefix(arg, "--ancestry-path=", &optarg)) {
2300 + struct commit *c;
2301 + struct object_id oid;
2302 + const char *msg = _("could not get commit for ancestry-path argument %s");
2303 +
2304 + revs->ancestry_path = 1;
2305 + revs->simplify_history = 0;
2306 + revs->limited = 1;
2307 +
2308 + if (repo_get_oid_committish(revs->repo, optarg, &oid))
2309 + return error(msg, optarg);
2310 + get_reference(revs, optarg, &oid, ANCESTRY_PATH);
2311 + c = lookup_commit_reference(revs->repo, &oid);
2312 + if (!c)
2313 + return error(msg, optarg);
2314 + commit_list_insert(c, &revs->ancestry_path_bottoms);
2315 } else if (!strcmp(arg, "-g") || !strcmp(arg, "--walk-reflogs")) {
2316 init_reflog_walk(&revs->reflog_info);
2317 } else if (!strcmp(arg, "--default")) {
@@ -2993,6 +3021,7 @@ static void release_revisions_topo_walk_info(struct topo_walk_info *info);
3021 void release_revisions(struct rev_info *revs)
3022 {
3023 free_commit_list(revs->commits);
3024 + free_commit_list(revs->ancestry_path_bottoms);
3025 object_array_clear(&revs->pending);
3026 object_array_clear(&revs->boundary_commits);
3027 release_revisions_cmdline(&revs->cmdline);
revision.h
+9
@@ -48,6 +48,7 @@
48 */
49 #define NOT_USER_GIVEN (1u<<25)
50 #define TRACK_LINEAR (1u<<26)
51 +#define ANCESTRY_PATH (1u<<27)
52 #define ALL_REV_FLAGS (((1u<<11)-1) | NOT_USER_GIVEN | TRACK_LINEAR | PULL_MERGE)
53
54 #define DECORATE_SHORT_REFS 1
@@ -164,6 +165,13 @@ struct rev_info {
165 cherry_mark:1,
166 bisect:1,
167 ancestry_path:1,
168 +
169 + /* True if --ancestry-path was specified without an
170 + * argument. The bottom revisions are implicitly
171 + * the arguments in this case.
172 + */
173 + ancestry_path_implicit_bottoms:1,
174 +
175 first_parent_only:1,
176 exclude_first_parent_only:1,
177 line_level_traverse:1,
@@ -306,6 +314,7 @@ struct rev_info {
314 struct saved_parents *saved_parents_slab;
315
316 struct commit_list *previous_parents;
317 + struct commit_list *ancestry_path_bottoms;
318 const char *break_bar;
319
320 struct revision_sources *sources;
t/t6019-rev-list-ancestry-path.sh
+12 -2
@@ -8,8 +8,13 @@ test_description='--ancestry-path'
8 # / \
9 # A-------K---------------L--M
10 #
11 -# D..M == E F G H I J K L M
12 -# --ancestry-path D..M == E F H I J L M
11 +# D..M == E F G H I J K L M
12 +# --ancestry-path D..M == E F H I J L M
13 +# --ancestry-path=F D..M == E F J L M
14 +# --ancestry-path=G D..M == G H I J L M
15 +# --ancestry-path=H D..M == E G H I J L M
16 +# --ancestry-path=K D..M == K L M
17 +# --ancestry-path=K --ancestry-path=F D..M == E F J K L M
18 #
19 # D..M -- M.t == M
20 # --ancestry-path D..M -- M.t == M
@@ -70,6 +75,11 @@ test_ancestry () {
75 test_ancestry "D..M" "E F G H I J K L M"
76
77 test_ancestry "--ancestry-path D..M" "E F H I J L M"
78 +test_ancestry "--ancestry-path=F D..M" "E F J L M"
79 +test_ancestry "--ancestry-path=G D..M" "G H I J L M"
80 +test_ancestry "--ancestry-path=H D..M" "E G H I J L M"
81 +test_ancestry "--ancestry-path=K D..M" "K L M"
82 +test_ancestry "--ancestry-path=F --ancestry-path=K D..M" "E F J K L M"
83
84 test_ancestry "D..M -- M.t" "M"
85 test_ancestry "--ancestry-path D..M -- M.t" "M"