All of lore.kernel.org
 help / color / mirror / Atom feed
From: Junio C Hamano <gitster@pobox.com>
To: Mirko Faina <mroik@delayed.space>
Cc: git@vger.kernel.org, "Jeff King" <peff@peff.net>,
	"Jean-Noël Avila" <jn.avila@free.fr>,
	"Patrick Steinhardt" <ps@pks.im>, "Tian Yuchen" <cat@malon.dev>,
	"Ben Knoble" <ben.knoble@gmail.com>,
	"Johannes Sixt" <j6t@kdbg.org>,
	"Chris Torek" <chris.torek@gmail.com>
Subject: [PATCH v9] revision.c: implement --max-count-oldest
Date: Tue, 02 Jun 2026 06:53:10 +0900	[thread overview]
Message-ID: <xmqq4ijm3p2x.fsf@gitster.g> (raw)
In-Reply-To: <ag3kJ_xKY6584De4@exploit> (Mirko Faina's message of "Wed, 20 May 2026 18:42:36 +0200")

Mirko Faina <mroik@delayed.space> writes:

> On Wed, May 20, 2026 at 03:02:34PM +0900, Junio C Hamano wrote:
>> 
>> This breaks CI
>> 
>>   https://github.com/git/git/actions/runs/26138986677/job/76880268854#step:4:2072
>> 
>> Squash something like this to fix.
>>  ...
>
> Sorry about that. And thank you for the fix.

It has been a while, and we saw no further comments by other
reviewers.

Perhaps we should declare a victory and mark the topic for 'next'.

------ >8 ------
From: Mirko Faina <mroik@delayed.space>
Date: Tue, 19 May 2026 02:55:22 +0200

"--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>
---
 Documentation/rev-list-options.adoc |   5 +-
 revision.c                          | 111 +++++++++++++++++++++++++++-
 revision.h                          |   2 +
 t/t4202-log.sh                      |  40 ++++++++++
 4 files changed, 154 insertions(+), 4 deletions(-)

diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index 2d195a1474..e8c88d0f1c 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -16,7 +16,10 @@ ordering and formatting options, such as `--reverse`.
 `-<number>`::
 `-n <number>`::
 `--max-count=<number>`::
-	Limit the output to _<number>_ commits.
+	Limit the output to the first _<number>_ commits that would be shown.
+
+`--max-count-oldest=<number>`::
+	Limit the output to the last _<number>_ commits that would be shown.
 
 `--skip=<number>`::
 	Skip _<number>_ commits before starting to show the commit output.
diff --git a/revision.c b/revision.c
index 599b3a66c3..5d53db3152 100644
--- a/revision.c
+++ b/revision.c
@@ -2339,10 +2339,28 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 	}
 
 	if ((argcount = parse_long_opt("max-count", argv, &optarg))) {
+		if (revs->max_count_type == 1)
+			die_for_incompatible_opt2(1, "--max-count", 1,
+						  "--max-count-oldest");
 		revs->max_count = parse_count(optarg);
 		revs->no_walk = 0;
+		revs->max_count_type = 0;
 		return argcount;
+	} else if ((argcount = parse_long_opt("max-count-oldest", argv, &optarg))) {
+		if (revs->max_count_type == 0 && revs->max_count != -1)
+			die_for_incompatible_opt2(1, "--max-count", 1,
+						  "--max-count-oldest");
+		if (revs->skip_count > 0)
+			die_for_incompatible_opt2(1, "--skip", 1,
+						  "--max-count-oldest");
+		revs->max_count = parse_count(optarg);
+		revs->no_walk = 0;
+		revs->max_count_type = 1;
+		revs->max_count_stage = 0;
 	} else if ((argcount = parse_long_opt("skip", argv, &optarg))) {
+		if (revs->max_count_type == 1)
+			die_for_incompatible_opt2(1, "--skip", 1,
+						  "--max-count-oldest");
 		revs->skip_count = parse_count(optarg);
 		return argcount;
 	} else if ((*arg == '-') && isdigit(arg[1])) {
@@ -4521,15 +4539,91 @@ static struct commit *get_revision_internal(struct rev_info *revs)
 	return c;
 }
 
+static void retrieve_oldest_commits(struct rev_info *revs,
+				    struct commit_list **queue)
+{
+	struct commit *c;
+	int max_count = revs->max_count;
+	int queuei_count = 0;
+	int queueo_count = 0;
+	struct commit_list *queueo = NULL;
+	struct commit_list *queuei = NULL;
+	struct commit_list *reversed_queue = NULL;
+	struct commit_list *p;
+
+	revs->max_count = -1;
+	while ((c = get_revision_internal(revs))) {
+		/*
+		 * We need to reset SHOWN status otherwise --graph breaks.
+		 * It is fine to do, get_revision_internal() doesn't consider
+		 * children commits as they have been already processed and the
+		 * traversal happens only child to parent.
+		 *
+		 * We do this because the --graph machinery relies on the status
+		 * of the parents to decide how the printing will happen.
+		 *
+		 * We can't simply replace this instruction with a
+		 * graph_update() as it doesn't do the actualy printing, we'd
+		 * have to remove any commit that goes over the
+		 * --max-count-oldest limit from revs->graph.
+		 */
+		c->object.flags &= ~(SHOWN | CHILD_SHOWN);
+		commit_list_insert(c, &queuei);
+		if (!(c->object.flags & BOUNDARY))
+			queuei_count++;
+		while (queuei_count + queueo_count > max_count) {
+			if (!queueo_count) {
+				while ((c = pop_commit(&queuei))) {
+					commit_list_insert(c, &queueo);
+					queueo_count++;
+				}
+				queuei_count = 0;
+			}
+			c = pop_commit(&queueo);
+			queueo_count--;
+			/* We need to do this otherwise we'll discard the
+			 * commits that go over the --max-count-oldest limit but
+			 * not their respective boundaries. This matters only if
+			 * we're discarding the commit right before the boundary.
+			 */
+			for (p = c->parents; p; p = p->next)
+				p->item->object.flags &= ~CHILD_SHOWN;
+		}
+	}
+
+	while ((c = pop_commit(&queueo)))
+		commit_list_insert(c, &reversed_queue);
+	while ((c = pop_commit(&queuei)))
+		commit_list_insert(c, &queueo);
+	while ((c = pop_commit(&queueo)))
+		commit_list_insert(c, &reversed_queue);
+
+	while ((c = pop_commit(&reversed_queue)))
+		commit_list_insert(c, queue);
+}
+
 struct commit *get_revision(struct rev_info *revs)
 {
 	struct commit *c;
 	struct commit_list *reversed;
+	struct commit_list *queue = NULL;
+	struct commit_list *p;
+
+	if (revs->max_count_type == 1 && !revs->max_count_stage) {
+		retrieve_oldest_commits(revs, &queue);
+		commit_list_free(revs->commits);
+		revs->commits = queue;
+		revs->max_count_stage = 1;
+	}
 
 	if (revs->reverse) {
 		reversed = NULL;
-		while ((c = get_revision_internal(revs)))
-			commit_list_insert(c, &reversed);
+		if (revs->max_count_type == 1)
+			while ((c = pop_commit(&revs->commits)))
+				commit_list_insert(c, &reversed);
+		else
+			while ((c = get_revision_internal(revs)))
+				commit_list_insert(c, &reversed);
 		commit_list_free(revs->commits);
 		revs->commits = reversed;
 		revs->reverse = 0;
@@ -4543,7 +4637,18 @@ struct commit *get_revision(struct rev_info *revs)
 		return c;
 	}
 
-	c = get_revision_internal(revs);
+	if (revs->max_count_stage) {
+		c = pop_commit(&revs->commits);
+		if (c) {
+			c->object.flags |= SHOWN;
+			if (!(c->object.flags & BOUNDARY))
+				for (p = c->parents; p; p = p->next)
+					p->item->object.flags |= CHILD_SHOWN;
+		}
+	} else {
+		c = get_revision_internal(revs);
+	}
+
 	if (c && revs->graph)
 		graph_update(revs->graph, c);
 	if (!c) {
diff --git a/revision.h b/revision.h
index 584f1338b5..e157463cb1 100644
--- a/revision.h
+++ b/revision.h
@@ -309,6 +309,8 @@ struct rev_info {
 	/* special limits */
 	int skip_count;
 	int max_count;
+	unsigned int max_count_type:1;
+	unsigned int max_count_stage:1;
 	timestamp_t max_age;
 	timestamp_t max_age_as_filter;
 	timestamp_t min_age;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 05cee9e41b..75edb0eb38 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -1882,6 +1882,46 @@ test_expect_success 'log --graph with --name-status' '
 	test_cmp_graph --name-status tangle..reach
 '
 
+test_expect_success 'log --max-count-oldest=3 --oneline' '
+	test_when_finished rm expect &&
+	git log --oneline | tail -n3 >expect &&
+	git log --oneline --max-count-oldest=3 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --max-count-oldest=3 --reverse --oneline' '
+	test_when_finished rm expect &&
+	git log --oneline --reverse | head -n3 >expect &&
+	git log --oneline --max-count-oldest=3 --reverse >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --max-count-oldest with --max-count' '
+	test_when_finished rm stderr &&
+	test_must_fail git log --max-count-oldest=3 --max-count=3 2>stderr &&
+	test_grep "cannot be used together" stderr
+'
+
+test_expect_success 'log --max-count-oldest with --skip' '
+	test_when_finished rm stderr &&
+	test_must_fail git log --max-count-oldest=3 --skip=1 2>stderr &&
+	test_grep "cannot be used together" stderr
+'
+
+test_expect_success 'log --max-count-oldest=1000 --graph --boundary' '
+	test_when_finished rm expect actual &&
+	git log --graph --boundary >expect &&
+	git log --max-count-oldest=1000 --graph --boundary >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --oneline --graph --boundary --max-count-oldest=1' '
+	test_when_finished rm -f actual &&
+	git log --oneline --graph --boundary --max-count-oldest=1 \
+		HEAD~1..HEAD >actual &&
+	test_line_count = 2 actual
+'
+
 cat >expect <<-\EOF
 * reach
 |
-- 
2.54.0-514-g9d901a57fc


  reply	other threads:[~2026-06-01 21:53 UTC|newest]

Thread overview: 63+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-18 16:47 [PATCH] revision.c: implement --reverse=before for walks Mirko Faina
2026-04-18 18:20 ` Tian Yuchen
2026-04-18 18:42   ` Mirko Faina
2026-04-18 18:51     ` Mirko Faina
2026-04-20 16:06   ` Junio C Hamano
2026-04-20 17:08     ` Tian Yuchen
2026-04-20 23:50     ` Mirko Faina
2026-04-19 12:06 ` Ben Knoble
2026-04-19 18:11   ` Mirko Faina
2026-04-19 19:12     ` D. Ben Knoble
2026-04-19 20:31       ` Mirko Faina
2026-04-20  0:21         ` Jeff King
2026-04-20  9:33           ` Mirko Faina
2026-04-20 10:30             ` Mirko Faina
2026-04-21  3:48             ` Jeff King
2026-04-22 18:24         ` D. Ben Knoble
2026-04-22 19:42           ` Mirko Faina
2026-04-20  0:04 ` Jeff King
2026-04-20  9:22   ` Mirko Faina
2026-04-22  0:28 ` [PATCH v2 0/2] " Mirko Faina
2026-04-22  0:30   ` Mirko Faina
2026-04-23 22:51   ` [PATCH v3 " Mirko Faina
2026-04-23 22:51     ` [PATCH v3 1/2] " Mirko Faina
2026-04-28  1:45       ` Junio C Hamano
2026-04-23 22:52     ` [PATCH v3 2/2] revision.c: reduce memory usage on reverse before Mirko Faina
2026-04-27  0:24     ` [PATCH v4 0/2] revision.c: implement --reverse=before for walks Mirko Faina
2026-04-27  0:24       ` [PATCH v4 1/2] " Mirko Faina
2026-04-27  6:45         ` Junio C Hamano
2026-04-27  7:33           ` Johannes Sixt
2026-04-27 12:30             ` Junio C Hamano
2026-04-27 13:58               ` Chris Torek
2026-04-27 16:48           ` [PATCH v4 1/2] revision.c: implement -b-reverse=before " Mirko Faina
2026-04-28  1:46             ` Junio C Hamano
2026-04-28  1:45         ` [PATCH v4 1/2] revision.c: implement --reverse=before " Junio C Hamano
2026-04-27  0:24       ` [PATCH v4 2/2] revision.c: reduce memory usage on reverse before Mirko Faina
2026-04-28  1:46         ` Junio C Hamano
2026-04-30 19:52       ` [PATCH v5] revision.c: implement --max-count-oldest Mirko Faina
2026-05-04  5:19         ` Junio C Hamano
2026-05-04 13:08           ` Mirko Faina
2026-05-05 21:54         ` [PATCH v6] " Mirko Faina
2026-05-06  6:45           ` Johannes Sixt
2026-05-06 12:54             ` Mirko Faina
2026-05-07  9:20               ` Junio C Hamano
2026-05-08  0:09                 ` Mirko Faina
2026-05-09 12:46           ` Jean-Noël AVILA
2026-05-10  0:41             ` Mirko Faina
2026-05-09 21:01           ` Junio C Hamano
2026-05-10  0:48             ` Mirko Faina
2026-05-15 23:29           ` [PATCH v7] " Mirko Faina
2026-05-19  0:55             ` [PATCH v8] " Mirko Faina
2026-05-19  1:04               ` Mirko Faina
2026-05-20  6:02               ` Junio C Hamano
2026-05-20 16:42                 ` Mirko Faina
2026-06-01 21:53                   ` Junio C Hamano [this message]
2026-06-02 17:42                     ` [PATCH v9] " Mirko Faina
2026-05-19  6:27             ` [PATCH v7] " Junio C Hamano
2026-05-19 10:57               ` Mirko Faina
2026-05-09 11:01         ` [PATCH v5] " Junio C Hamano
2026-05-10  0:36           ` Mirko Faina
2026-04-22  0:28 ` [PATCH v2 1/2] revision.c: implement --reverse=before for walks Mirko Faina
2026-04-22 22:44   ` Jeff King
2026-04-22 22:53     ` Mirko Faina
2026-04-22  0:28 ` [PATCH v2 2/2] revision.c: reduce memory usage on reverse before Mirko Faina

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=xmqq4ijm3p2x.fsf@gitster.g \
    --to=gitster@pobox.com \
    --cc=ben.knoble@gmail.com \
    --cc=cat@malon.dev \
    --cc=chris.torek@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=j6t@kdbg.org \
    --cc=jn.avila@free.fr \
    --cc=mroik@delayed.space \
    --cc=peff@peff.net \
    --cc=ps@pks.im \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.