threads / patch / 64531

patch, 3 partsExpand and enhance git-last-modified(1) documentation

Subject: [PATCH 0/3] Expand and enhance git-last-modified(1) documentation

## tl;dr

36 messages between Nov 26, 2025 and Feb 3, 2026. Diffs are folded; open one to read it.

replies: 35people: 6as markdown or json

Toon Claes· Nov 26, 2025, 06:09 UTC · lore

Option `-z` and `--max-depth` are not documented in git-last-modified(1) while they are pretty crucial. In these patches documentation is added in the man page and the `-h` output.

---
Toon Claes (3):
      last-modified: handle and document NUL termination
      last-modified: document option --max-depth
      last-modified: better document how depth in handled
 Documentation/git-last-modified.adoc | 71 +++++++++++++++++++++++++++++++++++-
 builtin/last-modified.c              | 23 ++++++++++--
 2 files changed, 90 insertions(+), 4 deletions(-)

--- base-commit: 6ab38b7e9cc7adafc304f3204616a4debd49c6e9 change-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4

Toon Claes· Nov 26, 2025, 06:09 UTC · re: Toon Claes · lore

[PATCH 1/3] last-modified: handle and document NUL termination

When option `-z` is provided to git-last-modified(1), each line is separated with a NUL instead of a newline. Document this properly and handle parsing of the option in the builtin itself.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-
 builtin/last-modified.c              | 13 ++++++++++---
 2 files changed, 30 insertions(+), 4 deletions(-)
Show changes to 2 files +30 −4

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 602843e095..cd4a5040b0 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
+git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
 
 DESCRIPTION
 -----------
@@ -32,6 +32,9 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`-z`::
+	Terminate each line with a _NUL_ rather than a newline.
+
 `<revision-range>`::
 	Only traverse commits in the specified revision range. When no
 	`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
@@ -44,6 +47,22 @@ OPTIONS
 	Without an optional path parameter, all files and subdirectories
 	in path traversal the are included in the output.
 
+OUTPUT
+------
+
+The output is in the format:
+
+------------
+ <oid> TAB <path> LF
+------------
+
+If a path contains any special characters, the path is C-style quoted. To
+avoid quoting, pass option `-z` to terminate each line with a NUL.
+
+------------
+ <oid> TAB <path> NUL
+------------
+
 SEE ALSO
 --------
 linkgit:git-blame[1],
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index b0ecbdc540..9206bbdc1d 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -23,6 +23,10 @@
 #define PARENT1 (1u<<16) /* used instead of SEEN */
 #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */
 
+#define LAST_MODIFIED_INIT { \
+	.line_termination = '\n', \
+}
+
 struct last_modified_entry {
 	struct hashmap_entry hashent;
 	struct object_id oid;
@@ -55,6 +59,7 @@ struct last_modified {
 	struct rev_info rev;
 	bool recursive;
 	bool show_trees;
+	int line_termination;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,
 		putchar('^');
 	printf("%s\t", oid_to_hex(&commit->object.oid));
 
-	if (lm->rev.diffopt.line_termination)
+	if (lm->line_termination)
 		write_name_quoted(path, stdout, '\n');
 	else
 		printf("%s%c", path, '\0');
@@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 		      struct repository *repo)
 {
 	int ret;
-	struct last_modified lm = { 0 };
+	struct last_modified lm = LAST_MODIFIED_INIT;
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] "
+		N_("git last-modified [--recursive] [--show-trees] [-z] "
 		   "[<revision-range>] [[--] <path>...]"),
 		NULL
 	};
@@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_SET_INT('z', NULL, &lm.line_termination,
+			N_("lines are separated with NUL character"), '\0'),
 		OPT_END()
 	};
 
-- 
2.51.2
Karthik Nayak· Nov 26, 2025, 13:03 UTC · re: Toon Claes · lore

Re: [PATCH 1/3] last-modified: handle and document NUL termination

Toon Claes <toon@iotcl.com> writes:
> When option `-z` is provided to git-last-modified(1), each line is
> separated with a NUL instead of a newline.
This line make it seem like the option already exists..
> Document this properly and
> handle parsing of the option in the builtin itself.
>

But this line says we add the option now. Perhaps this should be clearer.

Show 17 quoted lines
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>  Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-
>  builtin/last-modified.c              | 13 ++++++++++---
>  2 files changed, 30 insertions(+), 4 deletions(-)
>
> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
> index 602843e095..cd4a5040b0 100644
> --- a/Documentation/git-last-modified.adoc
> +++ b/Documentation/git-last-modified.adoc
> @@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
>  SYNOPSIS
>  --------
>  [synopsis]
> -git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
> +git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
>
This is a bit long now, let's wrap it.
Show 9 quoted lines
>  DESCRIPTION
>  -----------
> @@ -32,6 +32,9 @@ OPTIONS
>  	Show tree entries even when recursing into them. It has no effect
>  	without `--recursive`.
>
> +`-z`::
> +	Terminate each line with a _NUL_ rather than a newline.
> +
Nit: perhaps it is just me, but it would nicer to read if it said 'a
_NUL_ character'
Show 47 quoted lines
>  `<revision-range>`::
>  	Only traverse commits in the specified revision range. When no
>  	`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
> @@ -44,6 +47,22 @@ OPTIONS
>  	Without an optional path parameter, all files and subdirectories
>  	in path traversal the are included in the output.
>
> +OUTPUT
> +------
> +
> +The output is in the format:
> +
> +------------
> + <oid> TAB <path> LF
> +------------
> +
> +If a path contains any special characters, the path is C-style quoted. To
> +avoid quoting, pass option `-z` to terminate each line with a NUL.
> +
> +------------
> + <oid> TAB <path> NUL
> +------------
> +
>  SEE ALSO
>  --------
>  linkgit:git-blame[1],
> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
> index b0ecbdc540..9206bbdc1d 100644
> --- a/builtin/last-modified.c
> +++ b/builtin/last-modified.c
> @@ -23,6 +23,10 @@
>  #define PARENT1 (1u<<16) /* used instead of SEEN */
>  #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */
>
> +#define LAST_MODIFIED_INIT { \
> +	.line_termination = '\n', \
> +}
> +
>  struct last_modified_entry {
>  	struct hashmap_entry hashent;
>  	struct object_id oid;
> @@ -55,6 +59,7 @@ struct last_modified {
>  	struct rev_info rev;
>  	bool recursive;
>  	bool show_trees;
> +	int line_termination;
>
Wouldn't 'line_terminator' be a better name?
Show 8 quoted lines
>  	const char **all_paths;
>  	size_t all_paths_nr;
> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,
>  		putchar('^');
>  	printf("%s\t", oid_to_hex(&commit->object.oid));
>
> -	if (lm->rev.diffopt.line_termination)
> +	if (lm->line_termination)

So it did exist before. But this was parsed as part of diff_options, why make the change than?

Show 28 quoted lines
>  		write_name_quoted(path, stdout, '\n');
>  	else
>  		printf("%s%c", path, '\0');
> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  		      struct repository *repo)
>  {
>  	int ret;
> -	struct last_modified lm = { 0 };
> +	struct last_modified lm = LAST_MODIFIED_INIT;
>
>  	const char * const last_modified_usage[] = {
> -		N_("git last-modified [--recursive] [--show-trees] "
> +		N_("git last-modified [--recursive] [--show-trees] [-z] "
>  		   "[<revision-range>] [[--] <path>...]"),
>  		NULL
>  	};
> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  			 N_("recurse into subtrees")),
>  		OPT_BOOL('t', "show-trees", &lm.show_trees,
>  			 N_("show tree entries when recursing into subtrees")),
> +		OPT_SET_INT('z', NULL, &lm.line_termination,
> +			N_("lines are separated with NUL character"), '\0'),
>  		OPT_END()
>  	};
>
>
> --
> 2.51.2
Junio C Hamano· Nov 26, 2025, 16:57 UTC · re: Toon Claes · lore

Re: [PATCH 1/3] last-modified: handle and document NUL termination

Toon Claes <toon@iotcl.com> writes:
> When option `-z` is provided to git-last-modified(1), each line is
> separated with a NUL instead of a newline. Document this properly and
> handle parsing of the option in the builtin itself.

I think documenting does make sense, but it is not clear from the description why it is better to handle the option in the builtin itself, instead of letting the setup_revisions() take care of it.

Is it because after the command lets setup_revisions() to parse out the revision range, the command does not really let the revision machinery drive diffs and let it output anything (hence, even though rev.diffopt.line_termination is set from the command line, the calling builtin is the only one that pays attention, and the revision machinery and the diff machinery called from there does not pay attention to it?

And assuming that this new division of labor between the revision machinery and the subcommand makes sense (it needs to be explained better), the updated code does make sense to me.

But it looks suboptimal.  See below.
> +#define LAST_MODIFIED_INIT { \
> +	.line_termination = '\n', \
> +}

You have to introduce such a non-zero initialization, only because you pretend to accept _any_ byte here, and use it as the line termination character. If you were porting Git to ancient Macintosh, you could set this to '\r' and it would follow their text file convention there ;-)

But ...
Show 20 quoted lines
>  struct last_modified_entry {
>  	struct hashmap_entry hashent;
>  	struct object_id oid;
> @@ -55,6 +59,7 @@ struct last_modified {
>  	struct rev_info rev;
>  	bool recursive;
>  	bool show_trees;
> +	int line_termination;
>  
>  	const char **all_paths;
>  	size_t all_paths_nr;
> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,
>  		putchar('^');
>  	printf("%s\t", oid_to_hex(&commit->object.oid));
>  
> -	if (lm->rev.diffopt.line_termination)
> +	if (lm->line_termination)
>  		write_name_quoted(path, stdout, '\n');
>  	else
>  		printf("%s%c", path, '\0');

... you use hardcoded '\n' here, without allowing the value of line_termination to affect the termination character.

This is way suboptimal.  Instead, would it work if you add
	bool null_termination;
to the last_modified structure, and do
	if (!lm->null_termination)
		write_name_quoted(path, stdout, '\n');
	else
		printf("%s%c", path, '\0');
here?   Then
Show 6 quoted lines
> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  		      struct repository *repo)
>  {
>  	int ret;
> -	struct last_modified lm = { 0 };
> +	struct last_modified lm = LAST_MODIFIED_INIT;
You do not need this change, and
Show 12 quoted lines
>  	const char * const last_modified_usage[] = {
> -		N_("git last-modified [--recursive] [--show-trees] "
> +		N_("git last-modified [--recursive] [--show-trees] [-z] "
>  		   "[<revision-range>] [[--] <path>...]"),
>  		NULL
>  	};
> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  			 N_("recurse into subtrees")),
>  		OPT_BOOL('t', "show-trees", &lm.show_trees,
>  			 N_("show tree entries when recursing into subtrees")),
> +		OPT_SET_INT('z', NULL, &lm.line_termination,
> +			N_("lines are separated with NUL character"), '\0'),
This will become OPT_BOOL() to set the &lm.null_termination.
>  		OPT_END()
>  	};
Toon Claes· Nov 28, 2025, 18:50 UTC · re: Junio C Hamano · lore

Re: [PATCH 1/3] last-modified: handle and document NUL termination

Junio C Hamano <gitster@pobox.com> writes:
Show 9 quoted lines
> Toon Claes <toon@iotcl.com> writes:
>
>> When option `-z` is provided to git-last-modified(1), each line is
>> separated with a NUL instead of a newline. Document this properly and
>> handle parsing of the option in the builtin itself.
>
> I think documenting does make sense, but it is not clear from the
> description why it is better to handle the option in the builtin
> itself, instead of letting the setup_revisions() take care of it.

I know it's silly, but I wanted to feed these options to parse_options(). Doing this would make them show up in `git last-modified -h`.

Show 6 quoted lines
> Is it because after the command lets setup_revisions() to parse out
> the revision range, the command does not really let the revision
> machinery drive diffs and let it output anything (hence, even though
> rev.diffopt.line_termination is set from the command line, the calling
> builtin is the only one that pays attention, and the revision machinery
> and the diff machinery called from there does not pay attention to it?
That too, at least for this option. Not the other patch.
Show 82 quoted lines
> And assuming that this new division of labor between the revision
> machinery and the subcommand makes sense (it needs to be explained
> better), the updated code does make sense to me.
>
> But it looks suboptimal.  See below.
>
>> +#define LAST_MODIFIED_INIT { \
>> +	.line_termination = '\n', \
>> +}
>
> You have to introduce such a non-zero initialization, only because
> you pretend to accept _any_ byte here, and use it as the line
> termination character.  If you were porting Git to ancient Macintosh,
> you could set this to '\r' and it would follow their text file
> convention there ;-)
>
> But ...
>
>>  struct last_modified_entry {
>>  	struct hashmap_entry hashent;
>>  	struct object_id oid;
>> @@ -55,6 +59,7 @@ struct last_modified {
>>  	struct rev_info rev;
>>  	bool recursive;
>>  	bool show_trees;
>> +	int line_termination;
>>  
>>  	const char **all_paths;
>>  	size_t all_paths_nr;
>> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,
>>  		putchar('^');
>>  	printf("%s\t", oid_to_hex(&commit->object.oid));
>>  
>> -	if (lm->rev.diffopt.line_termination)
>> +	if (lm->line_termination)
>>  		write_name_quoted(path, stdout, '\n');
>>  	else
>>  		printf("%s%c", path, '\0');
>
> ... you use hardcoded '\n' here, without allowing the value of
> line_termination to affect the termination character.
>
> This is way suboptimal.  Instead, would it work if you add
>
> 	bool null_termination;
>
> to the last_modified structure, and do
>
> 	if (!lm->null_termination)
> 		write_name_quoted(path, stdout, '\n');
> 	else
> 		printf("%s%c", path, '\0');
>
> here?   Then
>
>> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>>  		      struct repository *repo)
>>  {
>>  	int ret;
>> -	struct last_modified lm = { 0 };
>> +	struct last_modified lm = LAST_MODIFIED_INIT;
>
> You do not need this change, and
>
>>  	const char * const last_modified_usage[] = {
>> -		N_("git last-modified [--recursive] [--show-trees] "
>> +		N_("git last-modified [--recursive] [--show-trees] [-z] "
>>  		   "[<revision-range>] [[--] <path>...]"),
>>  		NULL
>>  	};
>> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>>  			 N_("recurse into subtrees")),
>>  		OPT_BOOL('t', "show-trees", &lm.show_trees,
>>  			 N_("show tree entries when recursing into subtrees")),
>> +		OPT_SET_INT('z', NULL, &lm.line_termination,
>> +			N_("lines are separated with NUL character"), '\0'),
>
> This will become OPT_BOOL() to set the &lm.null_termination.
>
>>  		OPT_END()
>>  	};
>

I actually agree this is better. But I was following the pattern of `line_termination` other in various other places. I'll adapt it to use a bool instead.

-- 
Cheers,
Toon
Patrick Steinhardt· Dec 1, 2025, 10:32 UTC · re: Toon Claes · lore

Re: [PATCH 1/3] last-modified: handle and document NUL termination

On Fri, Nov 28, 2025 at 07:50:30PM +0100, Toon Claes wrote:
Show 15 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > Toon Claes <toon@iotcl.com> writes:
> >
> >> When option `-z` is provided to git-last-modified(1), each line is
> >> separated with a NUL instead of a newline. Document this properly and
> >> handle parsing of the option in the builtin itself.
> >
> > I think documenting does make sense, but it is not clear from the
> > description why it is better to handle the option in the builtin
> > itself, instead of letting the setup_revisions() take care of it.
> 
> I know it's silly, but I wanted to feed these options to
> parse_options(). Doing this would make them show up in `git
> last-modified -h`.

I think that reasoning makes sense, but it's certainly non-obvious from the commit message. So if you expand the commit message with an explanation the change becomes much more sensible.

Patrick
Toon Claes· Nov 26, 2025, 06:09 UTC · re: Toon Claes · lore

[PATCH 2/3] last-modified: document option --max-depth

Option --max-depth is supported by git-last-modified(1), because it was added to the diff machinery in a1dfa5448d (diff: teach tree-diff a max-depth parameter, 2025-08-07).

This option is useful for everyday use of the git-last-modified(1) command, so document it's existence in the man page and `-h` output.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc |  9 ++++++++-
 builtin/last-modified.c              | 12 +++++++++++-
 2 files changed, 19 insertions(+), 2 deletions(-)
Show changes to 2 files +19 −2

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index cd4a5040b0..8409daebe9 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
+	[<revision-range>] [[--] <path>...]
 
 DESCRIPTION
 -----------
@@ -32,6 +33,12 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`--max-depth=<depth>`::
+	For each pathspec given on the command line, descend at most `<depth>`
+	levels of directories. A negative value means no limit.
+	Setting a positive value implies `--recursive`.
+	Cannot be combined with wildcards in the pathspec.
+
 `-z`::
 	Terminate each line with a _NUL_ rather than a newline.
 
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 9206bbdc1d..ccb7ff66d4 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -25,6 +25,7 @@
 
 #define LAST_MODIFIED_INIT { \
 	.line_termination = '\n', \
+	.max_depth = -1, \
 }
 
 struct last_modified_entry {
@@ -60,6 +61,7 @@ struct last_modified {
 	bool recursive;
 	bool show_trees;
 	int line_termination;
+	int max_depth;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
 	lm->rev.diffopt.flags.recursive = lm->recursive;
 	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
 
+	if (lm->max_depth >= 0) {
+		lm->rev.diffopt.flags.recursive = 1;
+		lm->rev.diffopt.max_depth = lm->max_depth;
+		lm->rev.diffopt.max_depth_valid = 1;
+	}
+
 	argc = setup_revisions(argc, argv, &lm->rev, NULL);
 	if (argc > 1) {
 		error(_("unknown last-modified argument: %s"), argv[1]);
@@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = LAST_MODIFIED_INIT;
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] [-z] "
+		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
 		   "[<revision-range>] [[--] <path>...]"),
 		NULL
 	};
@@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
+			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
 		OPT_SET_INT('z', NULL, &lm.line_termination,
 			N_("lines are separated with NUL character"), '\0'),
 		OPT_END()
-- 
2.51.2
Karthik Nayak· Nov 26, 2025, 13:31 UTC · re: Toon Claes · lore

Re: [PATCH 2/3] last-modified: document option --max-depth

Toon Claes <toon@iotcl.com> writes:
> Option --max-depth is supported by git-last-modified(1), because it was
> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
> max-depth parameter, 2025-08-07).
>

At this point, does it make more sense to link the respective sections within 'Documentation/diff-options.adoc' as done by many other commands? This would ensure that we don't have to repeat the documentation.

Show 54 quoted lines
> This option is useful for everyday use of the git-last-modified(1)
> command, so document it's existence in the man page and `-h` output.
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>  Documentation/git-last-modified.adoc |  9 ++++++++-
>  builtin/last-modified.c              | 12 +++++++++++-
>  2 files changed, 19 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
> index cd4a5040b0..8409daebe9 100644
> --- a/Documentation/git-last-modified.adoc
> +++ b/Documentation/git-last-modified.adoc
> @@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
>  SYNOPSIS
>  --------
>  [synopsis]
> -git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
> +git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
> +	[<revision-range>] [[--] <path>...]
>
>  DESCRIPTION
>  -----------
> @@ -32,6 +33,12 @@ OPTIONS
>  	Show tree entries even when recursing into them. It has no effect
>  	without `--recursive`.
>
> +`--max-depth=<depth>`::
> +	For each pathspec given on the command line, descend at most `<depth>`
> +	levels of directories. A negative value means no limit.
> +	Setting a positive value implies `--recursive`.
> +	Cannot be combined with wildcards in the pathspec.
> +
>  `-z`::
>  	Terminate each line with a _NUL_ rather than a newline.
>
> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
> index 9206bbdc1d..ccb7ff66d4 100644
> --- a/builtin/last-modified.c
> +++ b/builtin/last-modified.c
> @@ -25,6 +25,7 @@
>
>  #define LAST_MODIFIED_INIT { \
>  	.line_termination = '\n', \
> +	.max_depth = -1, \
>  }
>
>  struct last_modified_entry {
> @@ -60,6 +61,7 @@ struct last_modified {
>  	bool recursive;
>  	bool show_trees;
>  	int line_termination;
> +	int max_depth;
>
Should this be signed?
Show 12 quoted lines
>  	const char **all_paths;
>  	size_t all_paths_nr;
> @@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
>  	lm->rev.diffopt.flags.recursive = lm->recursive;
>  	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
>
> +	if (lm->max_depth >= 0) {
> +		lm->rev.diffopt.flags.recursive = 1;
> +		lm->rev.diffopt.max_depth = lm->max_depth;
> +		lm->rev.diffopt.max_depth_valid = 1;
> +	}
> +

Or if our goal is to actually handle them within the 'git-last-modified(1)' command, shouldn't we ensure we don't allow any additional flags from being parsed as diffopt?

Currently other diffopts flags such as '--no-prefix', '--cc' and so on, are parsed even if they don't affect the output of 'git-last-modified(1)'. Shouldn't we disallow such behavior?

Show 24 quoted lines
>  	argc = setup_revisions(argc, argv, &lm->rev, NULL);
>  	if (argc > 1) {
>  		error(_("unknown last-modified argument: %s"), argv[1]);
> @@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  	struct last_modified lm = LAST_MODIFIED_INIT;
>
>  	const char * const last_modified_usage[] = {
> -		N_("git last-modified [--recursive] [--show-trees] [-z] "
> +		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
>  		   "[<revision-range>] [[--] <path>...]"),
>  		NULL
>  	};
> @@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  			 N_("recurse into subtrees")),
>  		OPT_BOOL('t', "show-trees", &lm.show_trees,
>  			 N_("show tree entries when recursing into subtrees")),
> +		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
> +			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
>  		OPT_SET_INT('z', NULL, &lm.line_termination,
>  			N_("lines are separated with NUL character"), '\0'),
>  		OPT_END()
>
> --
> 2.51.2
Toon Claes· Jan 16, 2026, 12:13 UTC · re: Karthik Nayak · lore

Re: [PATCH 2/3] last-modified: document option --max-depth

Karthik Nayak <karthik.188@gmail.com> writes:
Show 10 quoted lines
> Toon Claes <toon@iotcl.com> writes:
>
>> Option --max-depth is supported by git-last-modified(1), because it was
>> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
>> max-depth parameter, 2025-08-07).
>>
>
> At this point, does it make more sense to link the respective sections
> within 'Documentation/diff-options.adoc' as done by many other commands?
> This would ensure that we don't have to repeat the documentation.

Nah, there are way too many options that are not relevant for this command in there. I agree, it's annoying to duplicate things. But I don't know there's an easy way around that right now.

Show 56 quoted lines
>> This option is useful for everyday use of the git-last-modified(1)
>> command, so document it's existence in the man page and `-h` output.
>>
>> Signed-off-by: Toon Claes <toon@iotcl.com>
>> ---
>>  Documentation/git-last-modified.adoc |  9 ++++++++-
>>  builtin/last-modified.c              | 12 +++++++++++-
>>  2 files changed, 19 insertions(+), 2 deletions(-)
>>
>> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
>> index cd4a5040b0..8409daebe9 100644
>> --- a/Documentation/git-last-modified.adoc
>> +++ b/Documentation/git-last-modified.adoc
>> @@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
>>  SYNOPSIS
>>  --------
>>  [synopsis]
>> -git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
>> +git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
>> +	[<revision-range>] [[--] <path>...]
>>
>>  DESCRIPTION
>>  -----------
>> @@ -32,6 +33,12 @@ OPTIONS
>>  	Show tree entries even when recursing into them. It has no effect
>>  	without `--recursive`.
>>
>> +`--max-depth=<depth>`::
>> +	For each pathspec given on the command line, descend at most `<depth>`
>> +	levels of directories. A negative value means no limit.
>> +	Setting a positive value implies `--recursive`.
>> +	Cannot be combined with wildcards in the pathspec.
>> +
>>  `-z`::
>>  	Terminate each line with a _NUL_ rather than a newline.
>>
>> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
>> index 9206bbdc1d..ccb7ff66d4 100644
>> --- a/builtin/last-modified.c
>> +++ b/builtin/last-modified.c
>> @@ -25,6 +25,7 @@
>>
>>  #define LAST_MODIFIED_INIT { \
>>  	.line_termination = '\n', \
>> +	.max_depth = -1, \
>>  }
>>
>>  struct last_modified_entry {
>> @@ -60,6 +61,7 @@ struct last_modified {
>>  	bool recursive;
>>  	bool show_trees;
>>  	int line_termination;
>> +	int max_depth;
>>
>
> Should this be signed?
Yes, a negative number is allowed to set an unlimited max depth.
Show 17 quoted lines
>
>>  	const char **all_paths;
>>  	size_t all_paths_nr;
>> @@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
>>  	lm->rev.diffopt.flags.recursive = lm->recursive;
>>  	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
>>
>> +	if (lm->max_depth >= 0) {
>> +		lm->rev.diffopt.flags.recursive = 1;
>> +		lm->rev.diffopt.max_depth = lm->max_depth;
>> +		lm->rev.diffopt.max_depth_valid = 1;
>> +	}
>> +
>
> Or if our goal is to actually handle them within the
> 'git-last-modified(1)' command, shouldn't we ensure we don't allow any
> additional flags from being parsed as diffopt?
Is that possible?

All the magic that happens in setup_revision() is a little bit of a pain if you ask me.

> Currently other diffopts flags such as '--no-prefix', '--cc' and so on,
> are parsed even if they don't affect the output of
> 'git-last-modified(1)'. Shouldn't we disallow such behavior?

I think this would require a revamp of setup_revision(). But I'm happy to be proven otherwise.

Show 24 quoted lines
>>  	argc = setup_revisions(argc, argv, &lm->rev, NULL);
>>  	if (argc > 1) {
>>  		error(_("unknown last-modified argument: %s"), argv[1]);
>> @@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>>  	struct last_modified lm = LAST_MODIFIED_INIT;
>>
>>  	const char * const last_modified_usage[] = {
>> -		N_("git last-modified [--recursive] [--show-trees] [-z] "
>> +		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
>>  		   "[<revision-range>] [[--] <path>...]"),
>>  		NULL
>>  	};
>> @@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>>  			 N_("recurse into subtrees")),
>>  		OPT_BOOL('t', "show-trees", &lm.show_trees,
>>  			 N_("show tree entries when recursing into subtrees")),
>> +		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
>> +			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
>>  		OPT_SET_INT('z', NULL, &lm.line_termination,
>>  			N_("lines are separated with NUL character"), '\0'),
>>  		OPT_END()
>>
>> --
>> 2.51.2
-- 
Cheers,
Toon
Junio C Hamano· Nov 26, 2025, 19:49 UTC · re: Toon Claes · lore

Re: [PATCH 2/3] last-modified: document option --max-depth

Toon Claes <toon@iotcl.com> writes:
Show 12 quoted lines
> Option --max-depth is supported by git-last-modified(1), because it was
> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
> max-depth parameter, 2025-08-07).
>
> This option is useful for everyday use of the git-last-modified(1)
> command, so document it's existence in the man page and `-h` output.
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>  Documentation/git-last-modified.adoc |  9 ++++++++-
>  builtin/last-modified.c              | 12 +++++++++++-
>  2 files changed, 19 insertions(+), 2 deletions(-)
Does this step pass t0450?
    fixup! last-modified: document option --max-depth
Show changes to builtin/last-modified.c +2 −2
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index ccb7ff66d4..857554e70d 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -523,8 +523,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = LAST_MODIFIED_INIT;
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
-		   "[<revision-range>] [[--] <path>...]"),
+		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n"
+		   "    [<revision-range>] [[--] <path>...]"),
 		NULL
 	};
 
Toon Claes· Nov 28, 2025, 18:51 UTC · re: Junio C Hamano · lore

Re: [PATCH 2/3] last-modified: document option --max-depth

Junio C Hamano <gitster@pobox.com> writes:
Show 33 quoted lines
> Toon Claes <toon@iotcl.com> writes:
>
>> Option --max-depth is supported by git-last-modified(1), because it was
>> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
>> max-depth parameter, 2025-08-07).
>>
>> This option is useful for everyday use of the git-last-modified(1)
>> command, so document it's existence in the man page and `-h` output.
>>
>> Signed-off-by: Toon Claes <toon@iotcl.com>
>> ---
>>  Documentation/git-last-modified.adoc |  9 ++++++++-
>>  builtin/last-modified.c              | 12 +++++++++++-
>>  2 files changed, 19 insertions(+), 2 deletions(-)
>
> Does this step pass t0450?
>
>     fixup! last-modified: document option --max-depth
>
> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
> index ccb7ff66d4..857554e70d 100644
> --- a/builtin/last-modified.c
> +++ b/builtin/last-modified.c
> @@ -523,8 +523,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
>  	struct last_modified lm = LAST_MODIFIED_INIT;
>  
>  	const char * const last_modified_usage[] = {
> -		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
> -		   "[<revision-range>] [[--] <path>...]"),
> +		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n"
> +		   "    [<revision-range>] [[--] <path>...]"),
>  		NULL
>  	};

Derp, I should have checked that. I knew it could be a problem, but by the time I was about to send the patch I forgot. Sorry about that.

-- 
Cheers,
Toon
Toon Claes· Nov 26, 2025, 06:09 UTC · re: Toon Claes · lore

[PATCH 3/3] last-modified: better document how depth in handled

By default git-last-modified(1) only shows information about paths at the root level. This can be confusing. Clarify the command's behavior in the documentation.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 43 ++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
Show changes to Documentation/git-last-modified.adoc +43 −0
diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 8409daebe9..36f72954a5 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -27,6 +27,7 @@ OPTIONS
 `--recursive`::
 	Instead of showing tree entries, step into subtrees and show all entries
 	inside them recursively.
+	See the section "NOTES ABOUT DEPTH" below for more details.
 
 `-t`::
 `--show-trees`::
@@ -38,6 +39,7 @@ OPTIONS
 	levels of directories. A negative value means no limit.
 	Setting a positive value implies `--recursive`.
 	Cannot be combined with wildcards in the pathspec.
+	See the section "NOTES ABOUT DEPTH" below for more details.
 
 `-z`::
 	Terminate each line with a _NUL_ rather than a newline.
@@ -70,6 +72,47 @@ avoid quoting, pass option `-z` to terminate each line with a NUL.
  <oid> TAB <path> NUL
 ------------
 
+NOTES ABOUT DEPTH
+-----------------
+
+By default this command only shows information about paths at the root level.
+When a path that lives in a subtree is provided, information about the top-level
+subtree is printed. For example:
+
+------------
+$ git last-modified -- sub/file
+
+abcd1234abcd1234abcd1234abcd1234abcd1234 sub
+------------
+
+To get details about the exact path in a subtree, add option `--recursive`:
+
+------------
+$ git last-modified --recursive -- sub/file
+
+5678abca5678abca5678abca5678abca5678abca sub/file
+------------
+
+This comes with a downside. When the path provided is a tree itself, with
+option `--recursive` all paths in that subtree are printed too:
+
+------------
+$ git last-modified --recursive -- sub/subsub
+
+1234cdef1234cdef1234cdef1234cdef1234cdef sub/subsub/a
+3456cdef3456cdef3456cdef3456cdef3456cdef sub/subsub/b
+5678abcd5678abcd5678abcd5678abcd5678abcd sub/subsub/c
+------------
+
+To stop this command from traversing deeper into trees, add option
+`--max-depth=0`:
+
+------------
+$ git last-modified --recursive --max-depth=0 -- sub/subsub
+
+3456def3456def3456def3456def3456def3456b sub/subsub
+------------
+
 SEE ALSO
 --------
 linkgit:git-blame[1],
-- 
2.51.2
Eric Sunshine· Nov 26, 2025, 17:38 UTC · re: Toon Claes · lore

Re: [PATCH 3/3] last-modified: better document how depth in handled

On Wed, Nov 26, 2025 at 1:10 AM Toon Claes <toon@iotcl.com> wrote:
> last-modified: better document how depth in handled
s/in/is/
Show 5 quoted lines
> By default git-last-modified(1) only shows information about paths at
> the root level. This can be confusing. Clarify the command's behavior in
> the documentation.
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
Patrick Steinhardt· Dec 1, 2025, 10:32 UTC · re: Toon Claes · lore

Re: [PATCH 3/3] last-modified: better document how depth in handled

On Wed, Nov 26, 2025 at 07:09:45AM +0100, Toon Claes wrote:
> By default git-last-modified(1) only shows information about paths at
> the root level. This can be confusing. Clarify the command's behavior in
> the documentation.

Hm, that's confusing indeed. Is it possible for git-last-modified(1) to do the "right thing" automatically? That is, given "sub/file", show when that specific file has been last modified? Or is there a good (non-technical) reason it behaves the way it does?

Patrick
Toon Claes· Dec 2, 2025, 11:01 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 3/3] last-modified: better document how depth in handled

Patrick Steinhardt <ps@pks.im> writes:
> Hm, that's confusing indeed. Is it possible for git-last-modified(1) to
> do the "right thing" automatically? That is, given "sub/file", show when
> that specific file has been last modified? Or is there a good
> (non-technical) reason it behaves the way it does?

You bring up a good point. In the version of git-blame-tree(1) that GitHub did share, the `--recursive` flag is enabled by default. So if you pass a file path to the command, you'll get the "right thing". But as I am pointing out in this patch, if you pass a subtree, everything in that subtree is shown too. You could argue this is the "right thing".

Anyhow, in the version of git-last-modified(1) I submitted upstream, recursive is not enabled by default. My reason, at the time option --max-depth wasn't implemented yet. I submitted those changes in a separate series (these patches also originate from GitHub by the way). If those patches wouldn't land, I think always-on recursive behavior for git-last-modified(1) would be quite annoying.

So long story short, as git-last-modified(1) is still marked as "EXPERIMENTAL", shall we make recursive always-on?

-- 
Cheers,
Toon
Patrick Steinhardt· Dec 2, 2025, 17:14 UTC · re: Toon Claes · lore

Re: [PATCH 3/3] last-modified: better document how depth in handled

On Tue, Dec 02, 2025 at 12:01:18PM +0100, Toon Claes wrote:
Show 22 quoted lines
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > Hm, that's confusing indeed. Is it possible for git-last-modified(1) to
> > do the "right thing" automatically? That is, given "sub/file", show when
> > that specific file has been last modified? Or is there a good
> > (non-technical) reason it behaves the way it does?
> 
> You bring up a good point. In the version of git-blame-tree(1) that
> GitHub did share, the `--recursive` flag is enabled by default. So if
> you pass a file path to the command, you'll get the "right thing". But
> as I am pointing out in this patch, if you pass a subtree, everything in
> that subtree is shown too. You could argue this is the "right thing".
> 
> Anyhow, in the version of git-last-modified(1) I submitted upstream,
> recursive is not enabled by default. My reason, at the time option
> --max-depth wasn't implemented yet. I submitted those changes in a
> separate series (these patches also originate from GitHub by the way).
> If those patches wouldn't land, I think always-on recursive behavior for
> git-last-modified(1) would be quite annoying.
> 
> So long story short, as git-last-modified(1) is still marked as
> "EXPERIMENTAL", shall we make recursive always-on?

I think that is a good idea, as it sounds like it would make common use cases way more intuitive.

An alternative would be to default to a max-depth of 0 and stick to the non-recursive default. In that case, the command would work intuitively to the user, right? At least it would work intuitively if the user doesn't want a recursive listing.

So maybe we should combine these two improvements. That is, we use:
  - An infinite max-depth by default with the recursive bahviour.
  - A max-depth of 0 in the non-recursive case.

In this case, we'd always recursively list all entries by default, but in case the user explicitly doesn't want recursive behaviour we'd know to show the last modification date of all provided files.

I was briefly thinking that a max-depth of 1 might be a more obvious default for the non-recursive case. In that case, doing e.g. `git last-modified --no-recursive t` would list the contents of "t/", but nothing more. I'm a bit torn there though whether that really is a sufficient improvement over a max-depth of 0.

Patrick
Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation

We have had several reports[1][2] from users that have been thoroughly confused by the default behaviour of git-last-modified(1). Most importantly, when using the command on trees other than the root tree the requests are seemingly ignoring that request and instead blame the top-level subtree. For example:

    $ git last-modified t/lib-bash.sh
    acdfea4394db8e8b42f48e36f7726d64a909a89d t

This result is completely unexpected and feels buggy to anybody who is not deeply familiar with git-last-modified(1). Furthermore, users typically have a hard time to figure out the correct parameters.

This series changes the behavior to something that intuitively makes more sense.

The change of the default behavior is done in the last commit, in the lead-up commits changes in the documentation are made.

[1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com> [2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>

---
Changes in v2:
- Change the default --max-depth.
- Split up commits in smaller pieces.
- Add more testss
- Link to v1: https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com
Cc: "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com>
Cc: Gusted <gusted@codeberg.org>
---
Toon Claes (5):
      last-modified: document NUL termination
      last-modified: add option '-z' to help output
      last-modified: document option --max-depth
      last-modified: add option '--max-depth' to help output
      last-modified: change default max-depth to 0
 Documentation/git-last-modified.adoc | 29 ++++++++++++++++++++++++++++-
 builtin/last-modified.c              | 20 ++++++++++++++------
 t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 77 insertions(+), 7 deletions(-)
Range-diff versus v1:
1:  7f599cf0d1 ! 1:  4a9b8170c3 last-modified: handle and document NUL termination
    @@ Metadata
     Author: Toon Claes <toon@iotcl.com>
     
      ## Commit message ##
    -    last-modified: handle and document NUL termination
    +    last-modified: document NUL termination
     
    -    When option `-z` is provided to git-last-modified(1), each line is
    -    separated with a NUL instead of a newline. Document this properly and
    -    handle parsing of the option in the builtin itself.
    +    The command git-last-modified(1) already recognizes the option '-z', and
    +    similar to many other commands this will make the output NUL-terminated
    +    instead of using newlines. Although, this option is missing from the
    +    documentation, so add it.
     
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
    @@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show whe
      --------
      [synopsis]
     -git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
    -+git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
    ++git last-modified [--recursive] [--show-trees] [-z]
    ++		  [<revision-range>] [[--] <path>...]
      
      DESCRIPTION
      -----------
    @@ Documentation/git-last-modified.adoc: OPTIONS
      	without `--recursive`.
      
     +`-z`::
    -+	Terminate each line with a _NUL_ rather than a newline.
    ++	Terminate each line with a _NUL_ character rather than a newline.
     +
      `<revision-range>`::
      	Only traverse commits in the specified revision range. When no
    @@ Documentation/git-last-modified.adoc: OPTIONS
      linkgit:git-blame[1],
     
      ## builtin/last-modified.c ##
    -@@
    - #define PARENT1 (1u<<16) /* used instead of SEEN */
    - #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */
    - 
    -+#define LAST_MODIFIED_INIT { \
    -+	.line_termination = '\n', \
    -+}
    -+
    - struct last_modified_entry {
    - 	struct hashmap_entry hashent;
    - 	struct object_id oid;
    -@@ builtin/last-modified.c: struct last_modified {
    - 	struct rev_info rev;
    - 	bool recursive;
    - 	bool show_trees;
    -+	int line_termination;
    - 
    - 	const char **all_paths;
    - 	size_t all_paths_nr;
    -@@ builtin/last-modified.c: static void last_modified_emit(struct last_modified *lm,
    - 		putchar('^');
    - 	printf("%s\t", oid_to_hex(&commit->object.oid));
    - 
    --	if (lm->rev.diffopt.line_termination)
    -+	if (lm->line_termination)
    - 		write_name_quoted(path, stdout, '\n');
    - 	else
    - 		printf("%s%c", path, '\0');
     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    - 		      struct repository *repo)
    - {
    - 	int ret;
    --	struct last_modified lm = { 0 };
    -+	struct last_modified lm = LAST_MODIFIED_INIT;
    + 	struct last_modified lm = { 0 };
      
      	const char * const last_modified_usage[] = {
     -		N_("git last-modified [--recursive] [--show-trees] "
    -+		N_("git last-modified [--recursive] [--show-trees] [-z] "
    - 		   "[<revision-range>] [[--] <path>...]"),
    +-		   "[<revision-range>] [[--] <path>...]"),
    ++		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
    ++		   "                  [<revision-range>] [[--] <path>...]"),
      		NULL
      	};
    -@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    - 			 N_("recurse into subtrees")),
    - 		OPT_BOOL('t', "show-trees", &lm.show_trees,
    - 			 N_("show tree entries when recursing into subtrees")),
    -+		OPT_SET_INT('z', NULL, &lm.line_termination,
    -+			N_("lines are separated with NUL character"), '\0'),
    - 		OPT_END()
    - 	};
      
-:  ---------- > 2:  94efeb29b1 last-modified: add option '-z' to help output
-:  ---------- > 3:  5669ab25e8 last-modified: document option --max-depth
2:  f193241248 ! 4:  b3060f8b38 last-modified: document option --max-depth
    @@ Metadata
     Author: Toon Claes <toon@iotcl.com>
     
      ## Commit message ##
    -    last-modified: document option --max-depth
    +    last-modified: add option '--max-depth' to help output
     
    -    Option --max-depth is supported by git-last-modified(1), because it was
    -    added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
    -    max-depth parameter, 2025-08-07).
    +    In previous commit option '--max-depth' was added to the documentation.
    +    To have it also appear in the help output of `git last-modified -h`,
    +    move the handling of '--max-depth' to parse_options() in
    +    builtin/last-modified.c itself.
     
    -    This option is useful for everyday use of the git-last-modified(1)
    -    command, so document it's existence in the man page and `-h` output.
    +    It enables us to change default behavior in a subsequent commit.
     
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
    - ## Documentation/git-last-modified.adoc ##
    -@@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show when files were last modified
    - SYNOPSIS
    - --------
    - [synopsis]
    --git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]
    -+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
    -+	[<revision-range>] [[--] <path>...]
    - 
    - DESCRIPTION
    - -----------
    -@@ Documentation/git-last-modified.adoc: OPTIONS
    - 	Show tree entries even when recursing into them. It has no effect
    - 	without `--recursive`.
    - 
    -+`--max-depth=<depth>`::
    -+	For each pathspec given on the command line, descend at most `<depth>`
    -+	levels of directories. A negative value means no limit.
    -+	Setting a positive value implies `--recursive`.
    -+	Cannot be combined with wildcards in the pathspec.
    -+
    - `-z`::
    - 	Terminate each line with a _NUL_ rather than a newline.
    - 
    -
      ## builtin/last-modified.c ##
    -@@
    - 
    - #define LAST_MODIFIED_INIT { \
    - 	.line_termination = '\n', \
    -+	.max_depth = -1, \
    - }
    - 
    - struct last_modified_entry {
     @@ builtin/last-modified.c: struct last_modified {
      	bool recursive;
      	bool show_trees;
    - 	int line_termination;
    + 	bool null_termination;
     +	int max_depth;
      
      	const char **all_paths;
    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,
      	argc = setup_revisions(argc, argv, &lm->rev, NULL);
      	if (argc > 1) {
      		error(_("unknown last-modified argument: %s"), argv[1]);
    -@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    - 	struct last_modified lm = LAST_MODIFIED_INIT;
    - 
    - 	const char * const last_modified_usage[] = {
    --		N_("git last-modified [--recursive] [--show-trees] [-z] "
    -+		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] "
    - 		   "[<revision-range>] [[--] <path>...]"),
    - 		NULL
    - 	};
     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
      			 N_("recurse into subtrees")),
      		OPT_BOOL('t', "show-trees", &lm.show_trees,
      			 N_("show tree entries when recursing into subtrees")),
     +		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
     +			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
    - 		OPT_SET_INT('z', NULL, &lm.line_termination,
    - 			N_("lines are separated with NUL character"), '\0'),
    + 		OPT_BOOL('z', NULL, &lm.null_termination,
    + 			N_("lines are separated with NUL character")),
      		OPT_END()
    + 	};
    + 
    ++	/*
    ++	 * Set the default of a max-depth to "unset". This will change in a
    ++	 * subsequent commit.
    ++	 */
    ++	lm.max_depth = -1;
    ++
    + 	argc = parse_options(argc, argv, prefix, last_modified_options,
    + 			     last_modified_usage,
    + 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
3:  6773f073d6 < -:  ---------- last-modified: better document how depth in handled
-:  ---------- > 5:  41819f8732 last-modified: change default max-depth to 0

--- base-commit: 7264e61d87e58b9d0f5e6424c47c11e9657dfb75 change-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4

Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 1/5] last-modified: document NUL termination

The command git-last-modified(1) already recognizes the option '-z', and similar to many other commands this will make the output NUL-terminated instead of using newlines. Although, this option is missing from the documentation, so add it.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 22 +++++++++++++++++++++-
 builtin/last-modified.c              |  4 ++--
 2 files changed, 23 insertions(+), 3 deletions(-)
Show changes to 2 files +23 −3

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 602843e095..2e5f370c15 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
+git last-modified [--recursive] [--show-trees] [-z]
+		  [<revision-range>] [[--] <path>...]
 
 DESCRIPTION
 -----------
@@ -32,6 +33,9 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`-z`::
+	Terminate each line with a _NUL_ character rather than a newline.
+
 `<revision-range>`::
 	Only traverse commits in the specified revision range. When no
 	`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
@@ -44,6 +48,22 @@ OPTIONS
 	Without an optional path parameter, all files and subdirectories
 	in path traversal the are included in the output.
 
+OUTPUT
+------
+
+The output is in the format:
+
+------------
+ <oid> TAB <path> LF
+------------
+
+If a path contains any special characters, the path is C-style quoted. To
+avoid quoting, pass option `-z` to terminate each line with a NUL.
+
+------------
+ <oid> TAB <path> NUL
+------------
+
 SEE ALSO
 --------
 linkgit:git-blame[1],
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index c80f0535f6..cac66d03fd 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -510,8 +510,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = { 0 };
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] "
-		   "[<revision-range>] [[--] <path>...]"),
+		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
+		   "                  [<revision-range>] [[--] <path>...]"),
 		NULL
 	};
 
-- 
2.52.0
Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 2/5] last-modified: add option '-z' to help output

The parsing of option '-z' is done by diff_opt_parse(), which is called by setup_revisions(), and ends up filling in `struct diff_options::line_termination`. But that field isn't used by the diff machinery itself, only by builtin/last-modified.c to format the output.

To have '-z' also appear in the help output of `git last-modified -h`, move the handling of '-z' to parse_options() in builtin/last-modified.c itself.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 builtin/last-modified.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
Show changes to builtin/last-modified.c +6 −3
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index cac66d03fd..0d73384e45 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -55,6 +55,7 @@ struct last_modified {
 	struct rev_info rev;
 	bool recursive;
 	bool show_trees;
+	bool null_termination;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -165,10 +166,10 @@ static void last_modified_emit(struct last_modified *lm,
 		putchar('^');
 	printf("%s\t", oid_to_hex(&commit->object.oid));
 
-	if (lm->rev.diffopt.line_termination)
-		write_name_quoted(path, stdout, '\n');
-	else
+	if (lm->null_termination)
 		printf("%s%c", path, '\0');
+	else
+		write_name_quoted(path, stdout, '\n');
 }
 
 static void mark_path(const char *path, const struct object_id *oid,
@@ -520,6 +521,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_BOOL('z', NULL, &lm.null_termination,
+			N_("lines are separated with NUL character")),
 		OPT_END()
 	};
 
-- 
2.52.0
Junio C Hamano· Jan 16, 2026, 18:31 UTC · re: Toon Claes · lore

Re: [PATCH v2 2/5] last-modified: add option '-z' to help output

Toon Claes <toon@iotcl.com> writes:
> +		OPT_BOOL('z', NULL, &lm.null_termination,
> +			N_("lines are separated with NUL character")),

As this is a name of a file-scope static thing, I do not think it matters too much, but "&lm.null_termination" (instead of "nul_termination") that controls use of "NUL character" somewhat bothers me ;-).

Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 3/5] last-modified: document option --max-depth

Option --max-depth is supported by git-last-modified(1), because it was added to the diff machinery in a1dfa5448d (diff: teach tree-diff a max-depth parameter, 2025-08-07).

This option is useful for everyday use of the git-last-modified(1) command, so document it's existence in the man page.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 8 +++++++-
 builtin/last-modified.c              | 2 +-
 2 files changed, 8 insertions(+), 2 deletions(-)
Show changes to 2 files +8 −2

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 2e5f370c15..a3992db3f2 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [-z]
+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
 		  [<revision-range>] [[--] <path>...]
 
 DESCRIPTION
@@ -33,6 +33,12 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`--max-depth=<depth>`::
+	For each pathspec given on the command line, descend at most `<depth>`
+	levels of directories. A negative value means no limit.
+	Setting a positive value implies `--recursive`.
+	Cannot be combined with wildcards in the pathspec.
+
 `-z`::
 	Terminate each line with a _NUL_ character rather than a newline.
 
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 0d73384e45..324363b5ca 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -511,7 +511,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = { 0 };
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
+		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n"
 		   "                  [<revision-range>] [[--] <path>...]"),
 		NULL
 	};
-- 
2.52.0
Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 4/5] last-modified: add option '--max-depth' to help output

In previous commit option '--max-depth' was added to the documentation. To have it also appear in the help output of `git last-modified -h`, move the handling of '--max-depth' to parse_options() in builtin/last-modified.c itself.

It enables us to change default behavior in a subsequent commit.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
 builtin/last-modified.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
Show changes to builtin/last-modified.c +15 −0
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 324363b5ca..842700bc6a 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -56,6 +56,7 @@ struct last_modified {
 	bool recursive;
 	bool show_trees;
 	bool null_termination;
+	int max_depth;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -483,6 +484,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
 	lm->rev.diffopt.flags.recursive = lm->recursive;
 	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
 
+	if (lm->max_depth >= 0) {
+		lm->rev.diffopt.flags.recursive = 1;
+		lm->rev.diffopt.max_depth = lm->max_depth;
+		lm->rev.diffopt.max_depth_valid = 1;
+	}
+
 	argc = setup_revisions(argc, argv, &lm->rev, NULL);
 	if (argc > 1) {
 		error(_("unknown last-modified argument: %s"), argv[1]);
@@ -521,11 +528,19 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
+			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
 		OPT_BOOL('z', NULL, &lm.null_termination,
 			N_("lines are separated with NUL character")),
 		OPT_END()
 	};
 
+	/*
+	 * Set the default of a max-depth to "unset". This will change in a
+	 * subsequent commit.
+	 */
+	lm.max_depth = -1;
+
 	argc = parse_options(argc, argv, prefix, last_modified_options,
 			     last_modified_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
-- 
2.52.0
Junio C Hamano· Jan 16, 2026, 18:42 UTC · re: Toon Claes · lore

Re: [PATCH v2 4/5] last-modified: add option '--max-depth' to help output

Toon Claes <toon@iotcl.com> writes:
Show 6 quoted lines
> In previous commit option '--max-depth' was added to the documentation.
> To have it also appear in the help output of `git last-modified -h`,
> move the handling of '--max-depth' to parse_options() in
> builtin/last-modified.c itself.
>
> It enables us to change default behavior in a subsequent commit.

This split of the max-depth into two steps does not look right to me. If we are handling the option ourselves here, shouldn't we do so in the same step, so that "git cmd -h" and the synopsis section of the documentation match?

Toon Claes· Jan 16, 2026, 13:22 UTC · re: Toon Claes · lore

[PATCH v2 5/5] last-modified: change default max-depth to 0

By default git-last-modified(1) doesn't recurse into subtrees. So when the pathspec contained a path in a subtree, the command would only print the commit information about the parent tree of the path, like:

    $ git last-modified -- path/file
    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path

Change the default behavior to give commit information about the exact path instead:

    $ git last-modified -- path/file
    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path/file

To achieve this, the default max-depth is changed to 0 and recursive is always enabled.

The handling of option '-r' is modified to disable a max-depth, resulting in the behavior of this option to remain unchanged.

No existing tests were modified, because there didn't exist any tests covering the example above. But more tests are added to cover this now.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc |  3 ++-
 builtin/last-modified.c              | 16 +++-------------
 t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 40 insertions(+), 14 deletions(-)
Show changes to 3 files +40 −14

Documentation/git-last-modified.adoc, builtin/last-modified.c, t/t8020-last-modified.sh

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index a3992db3f2..57136baf3b 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -27,6 +27,7 @@ OPTIONS
 `--recursive`::
 	Instead of showing tree entries, step into subtrees and show all entries
 	inside them recursively.
+	This is identical as setting `--max-depth=-1`.
 
 `-t`::
 `--show-trees`::
@@ -36,7 +37,7 @@ OPTIONS
 `--max-depth=<depth>`::
 	For each pathspec given on the command line, descend at most `<depth>`
 	levels of directories. A negative value means no limit.
-	Setting a positive value implies `--recursive`.
+	The default depth is 0.
 	Cannot be combined with wildcards in the pathspec.
 
 `-z`::
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 842700bc6a..a10e711beb 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -481,14 +481,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
 	lm->rev.no_commit_id = 1;
 	lm->rev.diff = 1;
 	lm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;
-	lm->rev.diffopt.flags.recursive = lm->recursive;
+	lm->rev.diffopt.flags.recursive = 1;
 	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
-
-	if (lm->max_depth >= 0) {
-		lm->rev.diffopt.flags.recursive = 1;
-		lm->rev.diffopt.max_depth = lm->max_depth;
-		lm->rev.diffopt.max_depth_valid = 1;
-	}
+	lm->rev.diffopt.max_depth = lm->max_depth;
+	lm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;
 
 	argc = setup_revisions(argc, argv, &lm->rev, NULL);
 	if (argc > 1) {
@@ -535,12 +531,6 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 		OPT_END()
 	};
 
-	/*
-	 * Set the default of a max-depth to "unset". This will change in a
-	 * subsequent commit.
-	 */
-	lm.max_depth = -1;
-
 	argc = parse_options(argc, argv, prefix, last_modified_options,
 			     last_modified_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
diff --git a/t/t8020-last-modified.sh b/t/t8020-last-modified.sh
index 50f4312f71..3944d2e153 100755
--- a/t/t8020-last-modified.sh
+++ b/t/t8020-last-modified.sh
@@ -93,6 +93,41 @@ test_expect_success 'last-modified subdir recursive' '
 	EOF
 '
 
+test_expect_success 'last-modified subdir non-recursive' '
+	check_last_modified a <<-\EOF
+	3 a
+	EOF
+'
+
+test_expect_success 'last-modified path in subdir non-recursive' '
+	check_last_modified a/file <<-\EOF
+	2 a/file
+	EOF
+'
+
+test_expect_success 'last-modified subdir with wildcard non-recursive' '
+	check_last_modified a/* <<-\EOF
+	3 a/b
+	2 a/file
+	EOF
+'
+
+test_expect_success 'last-modified with negative max-depth' '
+	check_last_modified --max-depth=-1 <<-\EOF
+	3 a/b/file
+	2 a/file
+	1 file
+	EOF
+'
+
+test_expect_success 'last-modified with max-depth of 1' '
+	check_last_modified --max-depth=1 <<-\EOF
+	3 a/b
+	2 a/file
+	1 file
+	EOF
+'
+
 test_expect_success 'last-modified from non-HEAD commit' '
 	check_last_modified HEAD^ <<-\EOF
 	2 a
-- 
2.52.0
Junio C Hamano· Jan 16, 2026, 18:55 UTC · re: Toon Claes · lore

Re: [PATCH v2 5/5] last-modified: change default max-depth to 0

Toon Claes <toon@iotcl.com> writes:
Show 9 quoted lines
> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
> index a3992db3f2..57136baf3b 100644
> --- a/Documentation/git-last-modified.adoc
> +++ b/Documentation/git-last-modified.adoc
> @@ -27,6 +27,7 @@ OPTIONS
>  `--recursive`::
>  	Instead of showing tree entries, step into subtrees and show all entries
>  	inside them recursively.
> +	This is identical as setting `--max-depth=-1`.

When I heard that the default value of max-depth will be 0, the first thing I wondered was "how would I spell unlimited in the new world order?", and the documentation for "--max-depth", not "--recursive", would have been the place I expected to fish for necessary information.

Over there, there is "A negative value means no limit", so saying "identical as setting --max-depth to a negative value" here would match the description over there better, or the user will be left wonderign if "-1" is merely an example that is negative, or if it is more special than other negative values and if so in what way.

Show 7 quoted lines
> @@ -36,7 +37,7 @@ OPTIONS
>  `--max-depth=<depth>`::
>  	For each pathspec given on the command line, descend at most `<depth>`
>  	levels of directories. A negative value means no limit.
> -	Setting a positive value implies `--recursive`.
> +	The default depth is 0.
>  	Cannot be combined with wildcards in the pathspec.
Show 10 quoted lines
> diff --git a/builtin/last-modified.c b/builtin/last-modified.c
> index 842700bc6a..a10e711beb 100644
> --- a/builtin/last-modified.c
> +++ b/builtin/last-modified.c
> @@ -481,14 +481,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
>  	lm->rev.no_commit_id = 1;
>  	lm->rev.diff = 1;
>  	lm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;
> -	lm->rev.diffopt.flags.recursive = lm->recursive;
> +	lm->rev.diffopt.flags.recursive = 1;
Hmph, so this will always be recursive?
>  	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
> +	lm->rev.diffopt.max_depth = lm->max_depth;
> +	lm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;

Not saying --recursive would keep lm->recursive==0 and non-negative value of --max-depth will flip max_depth_valid on. Saying "--recursive" or giving a negative "--max-value" would make max_depth_valid false, and it allows traversal all the way down to leaves.

It may be correct, but feels quite convoluted. I wonder if we can get rid of lm->recursive altogether now as a clean-up, and have "--recursive" truly do what the documentation claimed is equivalent earlier, i.e. OPT_SET_INT(0, "recursive", &lm.max_depth, -1). Would that simplify the logic a bit and make it easier to reason about the logic around here, I wonder?

Kristoffer Haugsbakk· Jan 16, 2026, 13:34 UTC · re: Toon Claes · lore

Re: [PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation

On Fri, Jan 16, 2026, at 14:22, Toon Claes wrote:
Show 32 quoted lines
> We have had several reports[1][2] from users that have been thoroughly
> confused by the default behaviour of git-last-modified(1). Most
> importantly, when using the command on trees other than the root tree
> the requests are seemingly ignoring that request and instead blame the
> top-level subtree. For example:
>
>     $ git last-modified t/lib-bash.sh
>     acdfea4394db8e8b42f48e36f7726d64a909a89d t
>
> This result is completely unexpected and feels buggy to anybody who is
> not deeply familiar with git-last-modified(1). Furthermore, users
> typically have a hard time to figure out the correct parameters.
>
> This series changes the behavior to something that intuitively makes
> more sense.
>
> The change of the default behavior is done in the last commit, in the
> lead-up commits changes in the documentation are made.
>
> [1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com>
> [2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>
>
> ---
> Changes in v2:
> - Change the default --max-depth.
> - Split up commits in smaller pieces.
> - Add more testss
> - Link to v1:
> https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com
>
> Cc: "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com>
> Cc: Gusted <gusted@codeberg.org>
I think the Cc list got mixed up.
>
> ---
>[snip]
Toon Claes· Jan 20, 2026, 10:44 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 5 quoted lines
> On Fri, Jan 16, 2026, at 14:22, Toon Claes wrote:
>> Cc: "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com>
>> Cc: Gusted <gusted@codeberg.org>
>
> I think the Cc list got mixed up.

I Cc'd you because of https://lore.kernel.org/git/f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com/

-- 
Cheers,
Toon
Toon Claes· Jan 20, 2026, 21:47 UTC · re: Toon Claes · lore

[PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation

We have had several reports[1][2] from users that have been thoroughly confused by the default behaviour of git-last-modified(1). Most importantly, when using the command on trees other than the root tree the requests are seemingly ignoring that request and instead blame the top-level subtree. For example:

    $ git last-modified t/lib-bash.sh
    acdfea4394db8e8b42f48e36f7726d64a909a89d t

This result is completely unexpected and feels buggy to anybody who is not deeply familiar with git-last-modified(1). Furthermore, users typically have a hard time to figure out the correct parameters.

This series changes the behavior to something that intuitively makes more sense.

The change of the default behavior is done in the last commit, in the lead-up commits changes in the documentation are made.

[1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com> [2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>

---
Changes in v3:
- Rejoined the two commits about `-z and the two about `--max-depth`
- In the end, drop `struct last_modified::recursive`. This ensures
  the effect of `--recursive` or `--max-depth` depends on whatever comes
  last.
- Added a commit to rename `<path>` to `<pathspec>` in the docs.
- Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com
Changes in v2:
- Change the default --max-depth.
- Split up commits in smaller pieces.
- Add more testss
- Link to v1: https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com
Cc: Gusted <gusted@codeberg.org>
---
Toon Claes (4):
      last-modified: clarify in the docs the command takes a pathspec
      last-modified: document option '-z'
      last-modified: document option '--max-depth'
      last-modified: change default max-depth to 0
 Documentation/git-last-modified.adoc | 45 ++++++++++++++++++++++++++++--------
 builtin/last-modified.c              | 25 ++++++++++++--------
 t/t8020-last-modified.sh             | 35 ++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+), 18 deletions(-)
Range-diff versus v2:
-:  ---------- > 1:  1290dbb179 last-modified: clarify in the docs the command takes a pathspec
1:  2c9fa32799 ! 2:  8ba5c59ae8 last-modified: document NUL termination
    @@ Metadata
     Author: Toon Claes <toon@iotcl.com>
     
      ## Commit message ##
    -    last-modified: document NUL termination
    +    last-modified: document option '-z'
     
         The command git-last-modified(1) already recognizes the option '-z', and
         similar to many other commands this will make the output NUL-terminated
         instead of using newlines. Although, this option is missing from the
         documentation, so add it.
     
    +    In addition to that, to have '-z' also appear in the help output of `git
    +    last-modified -h`, move the handling of '-z' to parse_options() in
    +    builtin/last-modified.c itself.
    +
    +    Before, the parsing of option '-z' was done by diff_opt_parse(), which
    +    is called by setup_revisions(). That would fill in `struct
    +    diff_options::line_termination`, but that field was not used by the diff
    +    machinery itself. Thus it makes more sense to have the handling of that
    +    option completely in builtin/last-modified.c.
    +
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
      ## Documentation/git-last-modified.adoc ##
    @@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show whe
      SYNOPSIS
      --------
      [synopsis]
    --git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
    +-git last-modified [--recursive] [--show-trees]
     +git last-modified [--recursive] [--show-trees] [-z]
    -+		  [<revision-range>] [[--] <path>...]
    + 		  [<revision-range>] [[--] <pathspec>...]
      
      DESCRIPTION
    - -----------
     @@ Documentation/git-last-modified.adoc: OPTIONS
      	Show tree entries even when recursing into them. It has no effect
      	without `--recursive`.
    @@ Documentation/git-last-modified.adoc: OPTIONS
      	Only traverse commits in the specified revision range. When no
      	`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
     @@ Documentation/git-last-modified.adoc: OPTIONS
    - 	Without an optional path parameter, all files and subdirectories
    - 	in path traversal the are included in the output.
    + 	If no _<pathspec>_ is given, all files and subdirectories are included.
    + 	See linkgit:gitglossary[7] for details on pathspec syntax.
      
     +OUTPUT
     +------
    @@ Documentation/git-last-modified.adoc: OPTIONS
      linkgit:git-blame[1],
     
      ## builtin/last-modified.c ##
    +@@ builtin/last-modified.c: struct last_modified {
    + 	struct rev_info rev;
    + 	bool recursive;
    + 	bool show_trees;
    ++	bool nul_termination;
    + 
    + 	const char **all_paths;
    + 	size_t all_paths_nr;
    +@@ builtin/last-modified.c: static void last_modified_emit(struct last_modified *lm,
    + 		putchar('^');
    + 	printf("%s\t", oid_to_hex(&commit->object.oid));
    + 
    +-	if (lm->rev.diffopt.line_termination)
    +-		write_name_quoted(path, stdout, '\n');
    +-	else
    ++	if (lm->nul_termination)
    + 		printf("%s%c", path, '\0');
    ++	else
    ++		write_name_quoted(path, stdout, '\n');
    + }
    + 
    + static void mark_path(const char *path, const struct object_id *oid,
     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
      	struct last_modified lm = { 0 };
      
      	const char * const last_modified_usage[] = {
    --		N_("git last-modified [--recursive] [--show-trees] "
    --		   "[<revision-range>] [[--] <path>...]"),
    +-		N_("git last-modified [--recursive] [--show-trees]\n"
     +		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
    -+		   "                  [<revision-range>] [[--] <path>...]"),
    + 		   "                  [<revision-range>] [[--] <pathspec>...]"),
      		NULL
      	};
    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    + 			 N_("recurse into subtrees")),
    + 		OPT_BOOL('t', "show-trees", &lm.show_trees,
    + 			 N_("show tree entries when recursing into subtrees")),
    ++		OPT_BOOL('z', NULL, &lm.nul_termination,
    ++			 N_("lines are separated with NUL character")),
    + 		OPT_END()
    + 	};
      
2:  04589b4ccc < -:  ---------- last-modified: add option '-z' to help output
3:  a45fe991ff < -:  ---------- last-modified: document option --max-depth
4:  3103084474 ! 3:  a1f0178263 last-modified: add option '--max-depth' to help output
    @@ Metadata
     Author: Toon Claes <toon@iotcl.com>
     
      ## Commit message ##
    -    last-modified: add option '--max-depth' to help output
    +    last-modified: document option '--max-depth'
    +
    +    Option --max-depth is supported by git-last-modified(1), because it was
    +    added to the diff machinery in a1dfa5448d (diff: teach tree-diff a
    +    max-depth parameter, 2025-08-07).
    +
    +    This option is useful for everyday use of the git-last-modified(1)
    +    command, so document it's existence in the man page.
     
    -    In previous commit option '--max-depth' was added to the documentation.
         To have it also appear in the help output of `git last-modified -h`,
         move the handling of '--max-depth' to parse_options() in
    -    builtin/last-modified.c itself.
    -
    -    It enables us to change default behavior in a subsequent commit.
    +    builtin/last-modified.c itself. This prepares for the change in default
    +    behavior in the next commit.
     
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
    + ## Documentation/git-last-modified.adoc ##
    +@@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show when files were last modified
    + SYNOPSIS
    + --------
    + [synopsis]
    +-git last-modified [--recursive] [--show-trees] [-z]
    ++git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
    + 		  [<revision-range>] [[--] <pathspec>...]
    + 
    + DESCRIPTION
    +@@ Documentation/git-last-modified.adoc: OPTIONS
    + 	Show tree entries even when recursing into them. It has no effect
    + 	without `--recursive`.
    + 
    ++`--max-depth=<depth>`::
    ++	For each pathspec given on the command line, traverse at most `<depth>`
    ++	levels into subtrees. A negative value means no limit.
    ++	The default is 0, which shows all paths matching the pathspec
    ++	without descending into subtrees.
    ++
    + `-z`::
    + 	Terminate each line with a _NUL_ character rather than a newline.
    + 
    +
      ## builtin/last-modified.c ##
     @@ builtin/last-modified.c: struct last_modified {
      	bool recursive;
      	bool show_trees;
    - 	bool null_termination;
    + 	bool nul_termination;
     +	int max_depth;
      
      	const char **all_paths;
    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,
      	argc = setup_revisions(argc, argv, &lm->rev, NULL);
      	if (argc > 1) {
      		error(_("unknown last-modified argument: %s"), argv[1]);
    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    + 	struct last_modified lm = { 0 };
    + 
    + 	const char * const last_modified_usage[] = {
    +-		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
    ++		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n"
    + 		   "                  [<revision-range>] [[--] <pathspec>...]"),
    + 		NULL
    + 	};
     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
      			 N_("recurse into subtrees")),
      		OPT_BOOL('t', "show-trees", &lm.show_trees,
      			 N_("show tree entries when recursing into subtrees")),
     +		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
    -+			N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
    - 		OPT_BOOL('z', NULL, &lm.null_termination,
    - 			N_("lines are separated with NUL character")),
    ++			      N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
    + 		OPT_BOOL('z', NULL, &lm.nul_termination,
    + 			 N_("lines are separated with NUL character")),
      		OPT_END()
      	};
      
5:  3795e85ab5 ! 4:  81ce108196 last-modified: change default max-depth to 0
    @@ Commit message
     
      ## Documentation/git-last-modified.adoc ##
     @@ Documentation/git-last-modified.adoc: OPTIONS
    + 
    + `-r`::
      `--recursive`::
    - 	Instead of showing tree entries, step into subtrees and show all entries
    - 	inside them recursively.
    -+	This is identical as setting `--max-depth=-1`.
    +-	Instead of showing tree entries, step into subtrees and show all entries
    +-	inside them recursively.
    ++	Recursively traverse into all subtrees. By default, the command only
    ++	shows tree entries matching the `<pathspec>`. With this option, it
    ++	descends into subtrees and displays all entries within them.
    ++	Equivalent to `--max-depth=-1`.
      
      `-t`::
      `--show-trees`::
    -@@ Documentation/git-last-modified.adoc: OPTIONS
    - `--max-depth=<depth>`::
    - 	For each pathspec given on the command line, descend at most `<depth>`
    - 	levels of directories. A negative value means no limit.
    --	Setting a positive value implies `--recursive`.
    -+	The default depth is 0.
    - 	Cannot be combined with wildcards in the pathspec.
    +-	Show tree entries even when recursing into them. It has no effect
    +-	without `--recursive`.
    ++	Show tree entries even when recursing into them.
      
    - `-z`::
    + `--max-depth=<depth>`::
    + 	For each pathspec given on the command line, traverse at most `<depth>`
     
      ## builtin/last-modified.c ##
    +@@ builtin/last-modified.c: define_commit_slab(active_paths_for_commit, struct bitmap *);
    + struct last_modified {
    + 	struct hashmap paths;
    + 	struct rev_info rev;
    +-	bool recursive;
    + 	bool show_trees;
    + 	bool nul_termination;
    + 	int max_depth;
     @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm, struct repository *r,
      	lm->rev.no_commit_id = 1;
      	lm->rev.diff = 1;
    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,
     -		lm->rev.diffopt.max_depth_valid = 1;
     -	}
     +	lm->rev.diffopt.max_depth = lm->max_depth;
    -+	lm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;
    ++	lm->rev.diffopt.max_depth_valid = lm->max_depth >= 0;
      
      	argc = setup_revisions(argc, argv, &lm->rev, NULL);
      	if (argc > 1) {
    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
    + 	};
    + 
    + 	struct option last_modified_options[] = {
    +-		OPT_BOOL('r', "recursive", &lm.recursive,
    +-			 N_("recurse into subtrees")),
    ++		OPT_SET_INT('r', "recursive", &lm.max_depth,
    ++			    N_("recurse into subtrees"), -1),
    + 		OPT_BOOL('t', "show-trees", &lm.show_trees,
    + 			 N_("show tree entries when recursing into subtrees")),
    + 		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,
      		OPT_END()
      	};

--- base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c change-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4

Toon Claes· Jan 20, 2026, 21:47 UTC · re: Toon Claes · lore

[PATCH v3 1/4] last-modified: clarify in the docs the command takes a pathspec

The documentation mentions git-last-modified(1) takes `<path>...`, but that argument actually accepts a pathspec. Reword the documentation to reflect that.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 11 ++++++-----
 builtin/last-modified.c              |  4 ++--
 2 files changed, 8 insertions(+), 7 deletions(-)
Show changes to 2 files +8 −7

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 602843e095..7c3fd844b8 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
+git last-modified [--recursive] [--show-trees]
+		  [<revision-range>] [[--] <pathspec>...]
 
 DESCRIPTION
 -----------
@@ -39,10 +40,10 @@ OPTIONS
 	spell `<revision-range>`, see the 'Specifying Ranges' section of
 	linkgit:gitrevisions[7].
 
-`[--] <path>...`::
-	For each _<path>_ given, the commit which last modified it is returned.
-	Without an optional path parameter, all files and subdirectories
-	in path traversal the are included in the output.
+`[--] <pathspec>...`::
+	Show the commit that last modified each path matching _<pathspec>_.
+	If no _<pathspec>_ is given, all files and subdirectories are included.
+	See linkgit:gitglossary[7] for details on pathspec syntax.
 
 SEE ALSO
 --------
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index c80f0535f6..e38e0bc4ca 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -510,8 +510,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = { 0 };
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] "
-		   "[<revision-range>] [[--] <path>...]"),
+		N_("git last-modified [--recursive] [--show-trees]\n"
+		   "                  [<revision-range>] [[--] <pathspec>...]"),
 		NULL
 	};
 
-- 
2.52.0
Toon Claes· Jan 20, 2026, 21:47 UTC · re: Toon Claes · lore

[PATCH v3 2/4] last-modified: document option '-z'

The command git-last-modified(1) already recognizes the option '-z', and similar to many other commands this will make the output NUL-terminated instead of using newlines. Although, this option is missing from the documentation, so add it.

In addition to that, to have '-z' also appear in the help output of `git last-modified -h`, move the handling of '-z' to parse_options() in builtin/last-modified.c itself.

Before, the parsing of option '-z' was done by diff_opt_parse(), which is called by setup_revisions(). That would fill in `struct diff_options::line_termination`, but that field was not used by the diff machinery itself. Thus it makes more sense to have the handling of that option completely in builtin/last-modified.c.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-
 builtin/last-modified.c              | 11 +++++++----
 2 files changed, 27 insertions(+), 5 deletions(-)
Show changes to 2 files +27 −5

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 7c3fd844b8..3760fd33a1 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees]
+git last-modified [--recursive] [--show-trees] [-z]
 		  [<revision-range>] [[--] <pathspec>...]
 
 DESCRIPTION
@@ -33,6 +33,9 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`-z`::
+	Terminate each line with a _NUL_ character rather than a newline.
+
 `<revision-range>`::
 	Only traverse commits in the specified revision range. When no
 	`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
@@ -45,6 +48,22 @@ OPTIONS
 	If no _<pathspec>_ is given, all files and subdirectories are included.
 	See linkgit:gitglossary[7] for details on pathspec syntax.
 
+OUTPUT
+------
+
+The output is in the format:
+
+------------
+ <oid> TAB <path> LF
+------------
+
+If a path contains any special characters, the path is C-style quoted. To
+avoid quoting, pass option `-z` to terminate each line with a NUL.
+
+------------
+ <oid> TAB <path> NUL
+------------
+
 SEE ALSO
 --------
 linkgit:git-blame[1],
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index e38e0bc4ca..4060abfad3 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -55,6 +55,7 @@ struct last_modified {
 	struct rev_info rev;
 	bool recursive;
 	bool show_trees;
+	bool nul_termination;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -165,10 +166,10 @@ static void last_modified_emit(struct last_modified *lm,
 		putchar('^');
 	printf("%s\t", oid_to_hex(&commit->object.oid));
 
-	if (lm->rev.diffopt.line_termination)
-		write_name_quoted(path, stdout, '\n');
-	else
+	if (lm->nul_termination)
 		printf("%s%c", path, '\0');
+	else
+		write_name_quoted(path, stdout, '\n');
 }
 
 static void mark_path(const char *path, const struct object_id *oid,
@@ -510,7 +511,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = { 0 };
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees]\n"
+		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
 		   "                  [<revision-range>] [[--] <pathspec>...]"),
 		NULL
 	};
@@ -520,6 +521,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_BOOL('z', NULL, &lm.nul_termination,
+			 N_("lines are separated with NUL character")),
 		OPT_END()
 	};
 
-- 
2.52.0
Toon Claes· Jan 20, 2026, 21:47 UTC · re: Toon Claes · lore

[PATCH v3 3/4] last-modified: document option '--max-depth'

Option --max-depth is supported by git-last-modified(1), because it was added to the diff machinery in a1dfa5448d (diff: teach tree-diff a max-depth parameter, 2025-08-07).

This option is useful for everyday use of the git-last-modified(1) command, so document it's existence in the man page.

To have it also appear in the help output of `git last-modified -h`, move the handling of '--max-depth' to parse_options() in builtin/last-modified.c itself. This prepares for the change in default behavior in the next commit.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc |  8 +++++++-
 builtin/last-modified.c              | 17 ++++++++++++++++-
 2 files changed, 23 insertions(+), 2 deletions(-)
Show changes to 2 files +23 −2

Documentation/git-last-modified.adoc, builtin/last-modified.c

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 3760fd33a1..6f9b119bb6 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified
 SYNOPSIS
 --------
 [synopsis]
-git last-modified [--recursive] [--show-trees] [-z]
+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]
 		  [<revision-range>] [[--] <pathspec>...]
 
 DESCRIPTION
@@ -33,6 +33,12 @@ OPTIONS
 	Show tree entries even when recursing into them. It has no effect
 	without `--recursive`.
 
+`--max-depth=<depth>`::
+	For each pathspec given on the command line, traverse at most `<depth>`
+	levels into subtrees. A negative value means no limit.
+	The default is 0, which shows all paths matching the pathspec
+	without descending into subtrees.
+
 `-z`::
 	Terminate each line with a _NUL_ character rather than a newline.
 
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 4060abfad3..28fc77a0f8 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -56,6 +56,7 @@ struct last_modified {
 	bool recursive;
 	bool show_trees;
 	bool nul_termination;
+	int max_depth;
 
 	const char **all_paths;
 	size_t all_paths_nr;
@@ -483,6 +484,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
 	lm->rev.diffopt.flags.recursive = lm->recursive;
 	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
 
+	if (lm->max_depth >= 0) {
+		lm->rev.diffopt.flags.recursive = 1;
+		lm->rev.diffopt.max_depth = lm->max_depth;
+		lm->rev.diffopt.max_depth_valid = 1;
+	}
+
 	argc = setup_revisions(argc, argv, &lm->rev, NULL);
 	if (argc > 1) {
 		error(_("unknown last-modified argument: %s"), argv[1]);
@@ -511,7 +518,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	struct last_modified lm = { 0 };
 
 	const char * const last_modified_usage[] = {
-		N_("git last-modified [--recursive] [--show-trees] [-z]\n"
+		N_("git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n"
 		   "                  [<revision-range>] [[--] <pathspec>...]"),
 		NULL
 	};
@@ -521,11 +528,19 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 			 N_("recurse into subtrees")),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
+		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
+			      N_("maximum tree depth to recurse"), PARSE_OPT_NONEG),
 		OPT_BOOL('z', NULL, &lm.nul_termination,
 			 N_("lines are separated with NUL character")),
 		OPT_END()
 	};
 
+	/*
+	 * Set the default of a max-depth to "unset". This will change in a
+	 * subsequent commit.
+	 */
+	lm.max_depth = -1;
+
 	argc = parse_options(argc, argv, prefix, last_modified_options,
 			     last_modified_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
-- 
2.52.0
Toon Claes· Jan 20, 2026, 21:47 UTC · re: Toon Claes · lore

[PATCH v3 4/4] last-modified: change default max-depth to 0

By default git-last-modified(1) doesn't recurse into subtrees. So when the pathspec contained a path in a subtree, the command would only print the commit information about the parent tree of the path, like:

    $ git last-modified -- path/file
    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path

Change the default behavior to give commit information about the exact path instead:

    $ git last-modified -- path/file
    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path/file

To achieve this, the default max-depth is changed to 0 and recursive is always enabled.

The handling of option '-r' is modified to disable a max-depth, resulting in the behavior of this option to remain unchanged.

No existing tests were modified, because there didn't exist any tests covering the example above. But more tests are added to cover this now.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-last-modified.adoc |  9 +++++----
 builtin/last-modified.c              | 21 +++++----------------
 t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 45 insertions(+), 20 deletions(-)
Show changes to 3 files +45 −20

Documentation/git-last-modified.adoc, builtin/last-modified.c, t/t8020-last-modified.sh

diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc
index 6f9b119bb6..d7d16fc4f7 100644
--- a/Documentation/git-last-modified.adoc
+++ b/Documentation/git-last-modified.adoc
@@ -25,13 +25,14 @@ OPTIONS
 
 `-r`::
 `--recursive`::
-	Instead of showing tree entries, step into subtrees and show all entries
-	inside them recursively.
+	Recursively traverse into all subtrees. By default, the command only
+	shows tree entries matching the `<pathspec>`. With this option, it
+	descends into subtrees and displays all entries within them.
+	Equivalent to `--max-depth=-1`.
 
 `-t`::
 `--show-trees`::
-	Show tree entries even when recursing into them. It has no effect
-	without `--recursive`.
+	Show tree entries even when recursing into them.
 
 `--max-depth=<depth>`::
 	For each pathspec given on the command line, traverse at most `<depth>`
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 28fc77a0f8..f7f4c5109c 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -53,7 +53,6 @@ define_commit_slab(active_paths_for_commit, struct bitmap *);
 struct last_modified {
 	struct hashmap paths;
 	struct rev_info rev;
-	bool recursive;
 	bool show_trees;
 	bool nul_termination;
 	int max_depth;
@@ -481,14 +480,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,
 	lm->rev.no_commit_id = 1;
 	lm->rev.diff = 1;
 	lm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;
-	lm->rev.diffopt.flags.recursive = lm->recursive;
+	lm->rev.diffopt.flags.recursive = 1;
 	lm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;
-
-	if (lm->max_depth >= 0) {
-		lm->rev.diffopt.flags.recursive = 1;
-		lm->rev.diffopt.max_depth = lm->max_depth;
-		lm->rev.diffopt.max_depth_valid = 1;
-	}
+	lm->rev.diffopt.max_depth = lm->max_depth;
+	lm->rev.diffopt.max_depth_valid = lm->max_depth >= 0;
 
 	argc = setup_revisions(argc, argv, &lm->rev, NULL);
 	if (argc > 1) {
@@ -524,8 +519,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 	};
 
 	struct option last_modified_options[] = {
-		OPT_BOOL('r', "recursive", &lm.recursive,
-			 N_("recurse into subtrees")),
+		OPT_SET_INT('r', "recursive", &lm.max_depth,
+			    N_("recurse into subtrees"), -1),
 		OPT_BOOL('t', "show-trees", &lm.show_trees,
 			 N_("show tree entries when recursing into subtrees")),
 		OPT_INTEGER_F(0, "max-depth", &lm.max_depth,
@@ -535,12 +530,6 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,
 		OPT_END()
 	};
 
-	/*
-	 * Set the default of a max-depth to "unset". This will change in a
-	 * subsequent commit.
-	 */
-	lm.max_depth = -1;
-
 	argc = parse_options(argc, argv, prefix, last_modified_options,
 			     last_modified_usage,
 			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |
diff --git a/t/t8020-last-modified.sh b/t/t8020-last-modified.sh
index 50f4312f71..3944d2e153 100755
--- a/t/t8020-last-modified.sh
+++ b/t/t8020-last-modified.sh
@@ -93,6 +93,41 @@ test_expect_success 'last-modified subdir recursive' '
 	EOF
 '
 
+test_expect_success 'last-modified subdir non-recursive' '
+	check_last_modified a <<-\EOF
+	3 a
+	EOF
+'
+
+test_expect_success 'last-modified path in subdir non-recursive' '
+	check_last_modified a/file <<-\EOF
+	2 a/file
+	EOF
+'
+
+test_expect_success 'last-modified subdir with wildcard non-recursive' '
+	check_last_modified a/* <<-\EOF
+	3 a/b
+	2 a/file
+	EOF
+'
+
+test_expect_success 'last-modified with negative max-depth' '
+	check_last_modified --max-depth=-1 <<-\EOF
+	3 a/b/file
+	2 a/file
+	1 file
+	EOF
+'
+
+test_expect_success 'last-modified with max-depth of 1' '
+	check_last_modified --max-depth=1 <<-\EOF
+	3 a/b
+	2 a/file
+	1 file
+	EOF
+'
+
 test_expect_success 'last-modified from non-HEAD commit' '
 	check_last_modified HEAD^ <<-\EOF
 	2 a
-- 
2.52.0
Kristoffer Haugsbakk· Jan 25, 2026, 11:24 UTC · re: Toon Claes · lore

Re: [PATCH v3 4/4] last-modified: change default max-depth to 0

On Tue, Jan 20, 2026, at 22:47, Toon Claes wrote:
Show 24 quoted lines
> By default git-last-modified(1) doesn't recurse into subtrees. So when
> the pathspec contained a path in a subtree, the command would only print
> the commit information about the parent tree of the path, like:
>
>     $ git last-modified -- path/file
>     aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path
>
> Change the default behavior to give commit information about the exact
> path instead:
>
>     $ git last-modified -- path/file
>     aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9	path/file
>
> To achieve this, the default max-depth is changed to 0 and recursive is
> always enabled.
>
> The handling of option '-r' is modified to disable a max-depth,
> resulting in the behavior of this option to remain unchanged.
>
> No existing tests were modified, because there didn't exist any tests
> covering the example above. But more tests are added to cover this now.
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
Thanks. I think this makes more sense.
Show 11 quoted lines
>[snip]
> --- a/Documentation/git-last-modified.adoc
> +++ b/Documentation/git-last-modified.adoc
> @@ -25,13 +25,14 @@ OPTIONS
>
>  `-r`::
>  `--recursive`::
> -	Instead of showing tree entries, step into subtrees and show all entries
> -	inside them recursively.
> +	Recursively traverse into all subtrees. By default, the command only
> +	shows tree entries matching the `<pathspec>`. With this option, it

This should be s/`<pathspec>`/_<pathspec>_ since this is a `[synopsis]` doc. But this is in `next` so it will have to be left for another time.

>[snip]
Junio C Hamano· Jan 21, 2026, 18:40 UTC · re: Toon Claes · lore

Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation

Toon Claes <toon@iotcl.com> writes:
Show 7 quoted lines
> Changes in v3:
> - Rejoined the two commits about `-z and the two about `--max-depth`
> - In the end, drop `struct last_modified::recursive`. This ensures
>   the effect of `--recursive` or `--max-depth` depends on whatever comes
>   last.
> - Added a commit to rename `<path>` to `<pathspec>` in the docs.
> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com
This round looked reasonable to me.  Shall we mark it for 'next'?
Thanks.
Karthik Nayak· Feb 3, 2026, 09:58 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation

Junio C Hamano <gitster@pobox.com> writes:
Show 13 quoted lines
> Toon Claes <toon@iotcl.com> writes:
>
>> Changes in v3:
>> - Rejoined the two commits about `-z and the two about `--max-depth`
>> - In the end, drop `struct last_modified::recursive`. This ensures
>>   the effect of `--recursive` or `--max-depth` depends on whatever comes
>>   last.
>> - Added a commit to rename `<path>` to `<pathspec>` in the docs.
>> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com
>
> This round looked reasonable to me.  Shall we mark it for 'next'?
>
> Thanks.
I also did a round of review and think it is indeed ready for 'next'.
Thanks
Junio C Hamano· Feb 3, 2026, 17:42 UTC · re: Karthik Nayak · lore

Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation

Karthik Nayak <karthik.188@gmail.com> writes:
Show 17 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Toon Claes <toon@iotcl.com> writes:
>>
>>> Changes in v3:
>>> - Rejoined the two commits about `-z and the two about `--max-depth`
>>> - In the end, drop `struct last_modified::recursive`. This ensures
>>>   the effect of `--recursive` or `--max-depth` depends on whatever comes
>>>   last.
>>> - Added a commit to rename `<path>` to `<pathspec>` in the docs.
>>> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com
>>
>> This round looked reasonable to me.  Shall we mark it for 'next'?
>>
>> Thanks.
>
> I also did a round of review and think it is indeed ready for 'next'.
;-)

← back to recent threads