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
|