revision.c: implement --max-count-oldest

"--max-count" is a commit limiting option and sets a maximum amount of commits to be shown. If a user wants to see only the first N commits of the history (the oldest commits) they'd have to do something like git log $(git rev-list HEAD | tail -n N | head -n 1) This is not very user-friendly. Teach get_revision() the --max-count-oldest option. Signed-off-by: Mirko Faina <mroik@delayed.space> [jc: fixed up t4202 <xmqq7boy4o05.fsf@gitster.g>] Signed-off-by: Junio C Hamano <gitster@pobox.com>

Mirko Faina committed May 19, 2026 at 02:55 UTC bb4ce23284d3605c892fdf4fe349fe8773c813d2
4 files changed +154 -4
Documentation/rev-list-options.adoc
+4 -1
@@ -16,7 +16,10 @@ ordering and formatting options, such as `--reverse`.
16 `-<number>`::
17 `-n <number>`::
18 `--max-count=<number>`::
19 - Limit the output to _<number>_ commits.
19 + Limit the output to the first _<number>_ commits that would be shown.
20 +
21 +`--max-count-oldest=<number>`::
22 + Limit the output to the last _<number>_ commits that would be shown.
23
24 `--skip=<number>`::
25 Skip _<number>_ commits before starting to show the commit output.
revision.c
+108 -3
@@ -2339,10 +2339,28 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2339 }
2340
2341 if ((argcount = parse_long_opt("max-count", argv, &optarg))) {
2342 + if (revs->max_count_type == 1)
2343 + die_for_incompatible_opt2(1, "--max-count", 1,
2344 + "--max-count-oldest");
2345 revs->max_count = parse_count(optarg);
2346 revs->no_walk = 0;
2347 + revs->max_count_type = 0;
2348 return argcount;
2349 + } else if ((argcount = parse_long_opt("max-count-oldest", argv, &optarg))) {
2350 + if (revs->max_count_type == 0 && revs->max_count != -1)
2351 + die_for_incompatible_opt2(1, "--max-count", 1,
2352 + "--max-count-oldest");
2353 + if (revs->skip_count > 0)
2354 + die_for_incompatible_opt2(1, "--skip", 1,
2355 + "--max-count-oldest");
2356 + revs->max_count = parse_count(optarg);
2357 + revs->no_walk = 0;
2358 + revs->max_count_type = 1;
2359 + revs->max_count_stage = 0;
2360 } else if ((argcount = parse_long_opt("skip", argv, &optarg))) {
2361 + if (revs->max_count_type == 1)
2362 + die_for_incompatible_opt2(1, "--skip", 1,
2363 + "--max-count-oldest");
2364 revs->skip_count = parse_count(optarg);
2365 return argcount;
2366 } else if ((*arg == '-') && isdigit(arg[1])) {
@@ -4521,15 +4539,91 @@ static struct commit *get_revision_internal(struct rev_info *revs)
4539 return c;
4540 }
4541
4542 +static void retrieve_oldest_commits(struct rev_info *revs,
4543 + struct commit_list **queue)
4544 +{
4545 + struct commit *c;
4546 + int max_count = revs->max_count;
4547 + int queuei_count = 0;
4548 + int queueo_count = 0;
4549 + struct commit_list *queueo = NULL;
4550 + struct commit_list *queuei = NULL;
4551 + struct commit_list *reversed_queue = NULL;
4552 + struct commit_list *p;
4553 +
4554 + revs->max_count = -1;
4555 + while ((c = get_revision_internal(revs))) {
4556 + /*
4557 + * We need to reset SHOWN status otherwise --graph breaks.
4558 + * It is fine to do, get_revision_internal() doesn't consider
4559 + * children commits as they have been already processed and the
4560 + * traversal happens only child to parent.
4561 + *
4562 + * We do this because the --graph machinery relies on the status
4563 + * of the parents to decide how the printing will happen.
4564 + *
4565 + * We can't simply replace this instruction with a
4566 + * graph_update() as it doesn't do the actualy printing, we'd
4567 + * have to remove any commit that goes over the
4568 + * --max-count-oldest limit from revs->graph.
4569 + */
4570 + c->object.flags &= ~(SHOWN | CHILD_SHOWN);
4571 + commit_list_insert(c, &queuei);
4572 + if (!(c->object.flags & BOUNDARY))
4573 + queuei_count++;
4574 + while (queuei_count + queueo_count > max_count) {
4575 + if (!queueo_count) {
4576 + while ((c = pop_commit(&queuei))) {
4577 + commit_list_insert(c, &queueo);
4578 + queueo_count++;
4579 + }
4580 + queuei_count = 0;
4581 + }
4582 + c = pop_commit(&queueo);
4583 + queueo_count--;
4584 + /* We need to do this otherwise we'll discard the
4585 + * commits that go over the --max-count-oldest limit but
4586 + * not their respective boundaries. This matters only if
4587 + * we're discarding the commit right before the boundary.
4588 + */
4589 + for (p = c->parents; p; p = p->next)
4590 + p->item->object.flags &= ~CHILD_SHOWN;
4591 + }
4592 + }
4593 +
4594 + while ((c = pop_commit(&queueo)))
4595 + commit_list_insert(c, &reversed_queue);
4596 + while ((c = pop_commit(&queuei)))
4597 + commit_list_insert(c, &queueo);
4598 + while ((c = pop_commit(&queueo)))
4599 + commit_list_insert(c, &reversed_queue);
4600 +
4601 + while ((c = pop_commit(&reversed_queue)))
4602 + commit_list_insert(c, queue);
4603 +}
4604 +
4605 struct commit *get_revision(struct rev_info *revs)
4606 {
4607 struct commit *c;
4608 struct commit_list *reversed;
4609 + struct commit_list *queue = NULL;
4610 + struct commit_list *p;
4611 +
4612 + if (revs->max_count_type == 1 && !revs->max_count_stage) {
4613 + retrieve_oldest_commits(revs, &queue);
4614 + commit_list_free(revs->commits);
4615 + revs->commits = queue;
4616 + revs->max_count_stage = 1;
4617 + }
4618
4619 if (revs->reverse) {
4620 reversed = NULL;
4531 - while ((c = get_revision_internal(revs)))
4532 - commit_list_insert(c, &reversed);
4621 + if (revs->max_count_type == 1)
4622 + while ((c = pop_commit(&revs->commits)))
4623 + commit_list_insert(c, &reversed);
4624 + else
4625 + while ((c = get_revision_internal(revs)))
4626 + commit_list_insert(c, &reversed);
4627 commit_list_free(revs->commits);
4628 revs->commits = reversed;
4629 revs->reverse = 0;
@@ -4543,7 +4637,18 @@ struct commit *get_revision(struct rev_info *revs)
4637 return c;
4638 }
4639
4546 - c = get_revision_internal(revs);
4640 + if (revs->max_count_stage) {
4641 + c = pop_commit(&revs->commits);
4642 + if (c) {
4643 + c->object.flags |= SHOWN;
4644 + if (!(c->object.flags & BOUNDARY))
4645 + for (p = c->parents; p; p = p->next)
4646 + p->item->object.flags |= CHILD_SHOWN;
4647 + }
4648 + } else {
4649 + c = get_revision_internal(revs);
4650 + }
4651 +
4652 if (c && revs->graph)
4653 graph_update(revs->graph, c);
4654 if (!c) {
revision.h
+2
@@ -309,6 +309,8 @@ struct rev_info {
309 /* special limits */
310 int skip_count;
311 int max_count;
312 + unsigned int max_count_type:1;
313 + unsigned int max_count_stage:1;
314 timestamp_t max_age;
315 timestamp_t max_age_as_filter;
316 timestamp_t min_age;
t/t4202-log.sh
+40
@@ -1882,6 +1882,46 @@ test_expect_success 'log --graph with --name-status' '
1882 test_cmp_graph --name-status tangle..reach
1883 '
1884
1885 +test_expect_success 'log --max-count-oldest=3 --oneline' '
1886 + test_when_finished rm expect &&
1887 + git log --oneline | tail -n3 >expect &&
1888 + git log --oneline --max-count-oldest=3 >actual &&
1889 + test_cmp expect actual
1890 +'
1891 +
1892 +test_expect_success 'log --max-count-oldest=3 --reverse --oneline' '
1893 + test_when_finished rm expect &&
1894 + git log --oneline --reverse | head -n3 >expect &&
1895 + git log --oneline --max-count-oldest=3 --reverse >actual &&
1896 + test_cmp expect actual
1897 +'
1898 +
1899 +test_expect_success 'log --max-count-oldest with --max-count' '
1900 + test_when_finished rm stderr &&
1901 + test_must_fail git log --max-count-oldest=3 --max-count=3 2>stderr &&
1902 + test_grep "cannot be used together" stderr
1903 +'
1904 +
1905 +test_expect_success 'log --max-count-oldest with --skip' '
1906 + test_when_finished rm stderr &&
1907 + test_must_fail git log --max-count-oldest=3 --skip=1 2>stderr &&
1908 + test_grep "cannot be used together" stderr
1909 +'
1910 +
1911 +test_expect_success 'log --max-count-oldest=1000 --graph --boundary' '
1912 + test_when_finished rm expect actual &&
1913 + git log --graph --boundary >expect &&
1914 + git log --max-count-oldest=1000 --graph --boundary >actual &&
1915 + test_cmp expect actual
1916 +'
1917 +
1918 +test_expect_success 'log --oneline --graph --boundary --max-count-oldest=1' '
1919 + test_when_finished rm -f actual &&
1920 + git log --oneline --graph --boundary --max-count-oldest=1 \
1921 + HEAD~1..HEAD >actual &&
1922 + test_line_count = 2 actual
1923 +'
1924 +
1925 cat >expect <<-\EOF
1926 * reach
1927 |