{"thread":{"id":"64531","subject":"[PATCH 0/3] Expand and enhance git-last-modified(1) documentation","startedAt":"2025-11-26T06:10:01Z","lastAt":"2026-02-03T17:42:26Z","messageCount":36,"participants":["Toon Claes","Karthik Nayak","Junio C Hamano","Eric Sunshine","Patrick Steinhardt","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"531281","messageId":"20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com","threadId":"64531","inReplyTo":null,"subject":"[PATCH 0/3] Expand and enhance git-last-modified(1) documentation","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-26T06:09:42Z","receivedAt":"2025-11-26T06:10:01Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option `-z` and `--max-depth` are not documented in git-last-modified(1)\nwhile they are pretty crucial. In these patches documentation is added\nin the man page and the `-h` output.\n\n---\nToon Claes (3):\n      last-modified: handle and document NUL termination\n      last-modified: document option --max-depth\n      last-modified: better document how depth in handled\n\n Documentation/git-last-modified.adoc | 71 +++++++++++++++++++++++++++++++++++-\n builtin/last-modified.c              | 23 ++++++++++--\n 2 files changed, 90 insertions(+), 4 deletions(-)\n\n\n\n---\nbase-commit: 6ab38b7e9cc7adafc304f3204616a4debd49c6e9\nchange-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4\n\n"},{"id":"531282","messageId":"20251126-toon-last-modified-zzzz-v1-1-608350df0caa@iotcl.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com","subject":"[PATCH 1/3] last-modified: handle and document NUL termination","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-26T06:09:43Z","receivedAt":"2025-11-26T06:10:06Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"When option `-z` is provided to git-last-modified(1), each line is\nseparated with a NUL instead of a newline. Document this properly and\nhandle parsing of the option in the builtin itself.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-\n builtin/last-modified.c              | 13 ++++++++++---\n 2 files changed, 30 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 602843e095..cd4a5040b0 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n+git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,9 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`-z`::\n+\tTerminate each line with a _NUL_ rather than a newline.\n+\n `<revision-range>`::\n \tOnly traverse commits in the specified revision range. When no\n \t`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole\n@@ -44,6 +47,22 @@ OPTIONS\n \tWithout an optional path parameter, all files and subdirectories\n \tin path traversal the are included in the output.\n \n+OUTPUT\n+------\n+\n+The output is in the format:\n+\n+------------\n+ <oid> TAB <path> LF\n+------------\n+\n+If a path contains any special characters, the path is C-style quoted. To\n+avoid quoting, pass option `-z` to terminate each line with a NUL.\n+\n+------------\n+ <oid> TAB <path> NUL\n+------------\n+\n SEE ALSO\n --------\n linkgit:git-blame[1],\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex b0ecbdc540..9206bbdc1d 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -23,6 +23,10 @@\n #define PARENT1 (1u<<16) /* used instead of SEEN */\n #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */\n \n+#define LAST_MODIFIED_INIT { \\\n+\t.line_termination = '\\n', \\\n+}\n+\n struct last_modified_entry {\n \tstruct hashmap_entry hashent;\n \tstruct object_id oid;\n@@ -55,6 +59,7 @@ struct last_modified {\n \tstruct rev_info rev;\n \tbool recursive;\n \tbool show_trees;\n+\tint line_termination;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,\n \t\tputchar('^');\n \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n \n-\tif (lm->rev.diffopt.line_termination)\n+\tif (lm->line_termination)\n \t\twrite_name_quoted(path, stdout, '\\n');\n \telse\n \t\tprintf(\"%s%c\", path, '\\0');\n@@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t      struct repository *repo)\n {\n \tint ret;\n-\tstruct last_modified lm = { 0 };\n+\tstruct last_modified lm = LAST_MODIFIED_INIT;\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n \t\t   \"[<revision-range>] [[--] <path>...]\"),\n \t\tNULL\n \t};\n@@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n+\t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.51.2\n\n"},{"id":"531283","messageId":"20251126-toon-last-modified-zzzz-v1-2-608350df0caa@iotcl.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com","subject":"[PATCH 2/3] last-modified: document option --max-depth","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-26T06:09:44Z","receivedAt":"2025-11-26T06:10:10Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --max-depth is supported by git-last-modified(1), because it was\nadded to the diff machinery in a1dfa5448d (diff: teach tree-diff a\nmax-depth parameter, 2025-08-07).\n\nThis option is useful for everyday use of the git-last-modified(1)\ncommand, so document it's existence in the man page and `-h` output.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc |  9 ++++++++-\n builtin/last-modified.c              | 12 +++++++++++-\n 2 files changed, 19 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex cd4a5040b0..8409daebe9 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n+\t[<revision-range>] [[--] <path>...]\n \n DESCRIPTION\n -----------\n@@ -32,6 +33,12 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`--max-depth=<depth>`::\n+\tFor each pathspec given on the command line, descend at most `<depth>`\n+\tlevels of directories. A negative value means no limit.\n+\tSetting a positive value implies `--recursive`.\n+\tCannot be combined with wildcards in the pathspec.\n+\n `-z`::\n \tTerminate each line with a _NUL_ rather than a newline.\n \ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 9206bbdc1d..ccb7ff66d4 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -25,6 +25,7 @@\n \n #define LAST_MODIFIED_INIT { \\\n \t.line_termination = '\\n', \\\n+\t.max_depth = -1, \\\n }\n \n struct last_modified_entry {\n@@ -60,6 +61,7 @@ struct last_modified {\n \tbool recursive;\n \tbool show_trees;\n \tint line_termination;\n+\tint max_depth;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n \tlm->rev.diffopt.flags.recursive = lm->recursive;\n \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n \n+\tif (lm->max_depth >= 0) {\n+\t\tlm->rev.diffopt.flags.recursive = 1;\n+\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n+\t\tlm->rev.diffopt.max_depth_valid = 1;\n+\t}\n+\n \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n \tif (argc > 1) {\n \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n@@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = LAST_MODIFIED_INIT;\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n \t\t   \"[<revision-range>] [[--] <path>...]\"),\n \t\tNULL\n \t};\n@@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n+\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n \t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n \t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n \t\tOPT_END()\n\n-- \n2.51.2\n\n"},{"id":"531284","messageId":"20251126-toon-last-modified-zzzz-v1-3-608350df0caa@iotcl.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com","subject":"[PATCH 3/3] last-modified: better document how depth in handled","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-26T06:09:45Z","receivedAt":"2025-11-26T06:10:14Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"By default git-last-modified(1) only shows information about paths at\nthe root level. This can be confusing. Clarify the command's behavior in\nthe documentation.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 43 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 43 insertions(+)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 8409daebe9..36f72954a5 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -27,6 +27,7 @@ OPTIONS\n `--recursive`::\n \tInstead of showing tree entries, step into subtrees and show all entries\n \tinside them recursively.\n+\tSee the section \"NOTES ABOUT DEPTH\" below for more details.\n \n `-t`::\n `--show-trees`::\n@@ -38,6 +39,7 @@ OPTIONS\n \tlevels of directories. A negative value means no limit.\n \tSetting a positive value implies `--recursive`.\n \tCannot be combined with wildcards in the pathspec.\n+\tSee the section \"NOTES ABOUT DEPTH\" below for more details.\n \n `-z`::\n \tTerminate each line with a _NUL_ rather than a newline.\n@@ -70,6 +72,47 @@ avoid quoting, pass option `-z` to terminate each line with a NUL.\n  <oid> TAB <path> NUL\n ------------\n \n+NOTES ABOUT DEPTH\n+-----------------\n+\n+By default this command only shows information about paths at the root level.\n+When a path that lives in a subtree is provided, information about the top-level\n+subtree is printed. For example:\n+\n+------------\n+$ git last-modified -- sub/file\n+\n+abcd1234abcd1234abcd1234abcd1234abcd1234 sub\n+------------\n+\n+To get details about the exact path in a subtree, add option `--recursive`:\n+\n+------------\n+$ git last-modified --recursive -- sub/file\n+\n+5678abca5678abca5678abca5678abca5678abca sub/file\n+------------\n+\n+This comes with a downside. When the path provided is a tree itself, with\n+option `--recursive` all paths in that subtree are printed too:\n+\n+------------\n+$ git last-modified --recursive -- sub/subsub\n+\n+1234cdef1234cdef1234cdef1234cdef1234cdef sub/subsub/a\n+3456cdef3456cdef3456cdef3456cdef3456cdef sub/subsub/b\n+5678abcd5678abcd5678abcd5678abcd5678abcd sub/subsub/c\n+------------\n+\n+To stop this command from traversing deeper into trees, add option\n+`--max-depth=0`:\n+\n+------------\n+$ git last-modified --recursive --max-depth=0 -- sub/subsub\n+\n+3456def3456def3456def3456def3456def3456b sub/subsub\n+------------\n+\n SEE ALSO\n --------\n linkgit:git-blame[1],\n\n-- \n2.51.2\n\n"},{"id":"531296","messageId":"CAOLa=ZRn45fDriKvwVgLV5yXbKUr7bwLadHjemPjb+Mw2tHVqQ@mail.gmail.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-1-608350df0caa@iotcl.com","subject":"Re: [PATCH 1/3] last-modified: handle and document NUL termination","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-11-26T13:03:06Z","receivedAt":"2025-11-26T13:03:08Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> When option `-z` is provided to git-last-modified(1), each line is\n> separated with a NUL instead of a newline.\n\nThis line make it seem like the option already exists..\n\n> Document this properly and\n> handle parsing of the option in the builtin itself.\n>\n\nBut this line says we add the option now. Perhaps this should be\nclearer.\n\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-\n>  builtin/last-modified.c              | 13 ++++++++++---\n>  2 files changed, 30 insertions(+), 4 deletions(-)\n>\n> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\n> index 602843e095..cd4a5040b0 100644\n> --- a/Documentation/git-last-modified.adoc\n> +++ b/Documentation/git-last-modified.adoc\n> @@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n> +git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n>\n\nThis is a bit long now, let's wrap it.\n\n>  DESCRIPTION\n>  -----------\n> @@ -32,6 +32,9 @@ OPTIONS\n>  \tShow tree entries even when recursing into them. It has no effect\n>  \twithout `--recursive`.\n>\n> +`-z`::\n> +\tTerminate each line with a _NUL_ rather than a newline.\n> +\n\nNit: perhaps it is just me, but it would nicer to read if it said 'a\n_NUL_ character'\n\n>  `<revision-range>`::\n>  \tOnly traverse commits in the specified revision range. When no\n>  \t`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole\n> @@ -44,6 +47,22 @@ OPTIONS\n>  \tWithout an optional path parameter, all files and subdirectories\n>  \tin path traversal the are included in the output.\n>\n> +OUTPUT\n> +------\n> +\n> +The output is in the format:\n> +\n> +------------\n> + <oid> TAB <path> LF\n> +------------\n> +\n> +If a path contains any special characters, the path is C-style quoted. To\n> +avoid quoting, pass option `-z` to terminate each line with a NUL.\n> +\n> +------------\n> + <oid> TAB <path> NUL\n> +------------\n> +\n>  SEE ALSO\n>  --------\n>  linkgit:git-blame[1],\n> diff --git a/builtin/last-modified.c b/builtin/last-modified.c\n> index b0ecbdc540..9206bbdc1d 100644\n> --- a/builtin/last-modified.c\n> +++ b/builtin/last-modified.c\n> @@ -23,6 +23,10 @@\n>  #define PARENT1 (1u<<16) /* used instead of SEEN */\n>  #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */\n>\n> +#define LAST_MODIFIED_INIT { \\\n> +\t.line_termination = '\\n', \\\n> +}\n> +\n>  struct last_modified_entry {\n>  \tstruct hashmap_entry hashent;\n>  \tstruct object_id oid;\n> @@ -55,6 +59,7 @@ struct last_modified {\n>  \tstruct rev_info rev;\n>  \tbool recursive;\n>  \tbool show_trees;\n> +\tint line_termination;\n>\n\nWouldn't 'line_terminator' be a better name?\n\n>  \tconst char **all_paths;\n>  \tsize_t all_paths_nr;\n> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,\n>  \t\tputchar('^');\n>  \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n>\n> -\tif (lm->rev.diffopt.line_termination)\n> +\tif (lm->line_termination)\n\nSo it did exist before. But this was parsed as part of diff_options, why\nmake the change than?\n\n>  \t\twrite_name_quoted(path, stdout, '\\n');\n>  \telse\n>  \t\tprintf(\"%s%c\", path, '\\0');\n> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \t\t      struct repository *repo)\n>  {\n>  \tint ret;\n> -\tstruct last_modified lm = { 0 };\n> +\tstruct last_modified lm = LAST_MODIFIED_INIT;\n>\n>  \tconst char * const last_modified_usage[] = {\n> -\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n>  \t\t   \"[<revision-range>] [[--] <path>...]\"),\n>  \t\tNULL\n>  \t};\n> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \t\t\t N_(\"recurse into subtrees\")),\n>  \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n>  \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n> +\t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n> +\t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n>  \t\tOPT_END()\n>  \t};\n>\n>\n> --\n> 2.51.2\n"},{"id":"531298","messageId":"CAOLa=ZSbV6SeJ9orOz0T+oh3PVhYhTsaxrsYVkr+5q7i_tsVCw@mail.gmail.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-2-608350df0caa@iotcl.com","subject":"Re: [PATCH 2/3] last-modified: document option --max-depth","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-11-26T13:31:32Z","receivedAt":"2025-11-26T13:31:34Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> Option --max-depth is supported by git-last-modified(1), because it was\n> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n> max-depth parameter, 2025-08-07).\n>\n\nAt this point, does it make more sense to link the respective sections\nwithin 'Documentation/diff-options.adoc' as done by many other commands?\nThis would ensure that we don't have to repeat the documentation.\n\n> This option is useful for everyday use of the git-last-modified(1)\n> command, so document it's existence in the man page and `-h` output.\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-last-modified.adoc |  9 ++++++++-\n>  builtin/last-modified.c              | 12 +++++++++++-\n>  2 files changed, 19 insertions(+), 2 deletions(-)\n>\n> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\n> index cd4a5040b0..8409daebe9 100644\n> --- a/Documentation/git-last-modified.adoc\n> +++ b/Documentation/git-last-modified.adoc\n> @@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n> +git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n> +\t[<revision-range>] [[--] <path>...]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -32,6 +33,12 @@ OPTIONS\n>  \tShow tree entries even when recursing into them. It has no effect\n>  \twithout `--recursive`.\n>\n> +`--max-depth=<depth>`::\n> +\tFor each pathspec given on the command line, descend at most `<depth>`\n> +\tlevels of directories. A negative value means no limit.\n> +\tSetting a positive value implies `--recursive`.\n> +\tCannot be combined with wildcards in the pathspec.\n> +\n>  `-z`::\n>  \tTerminate each line with a _NUL_ rather than a newline.\n>\n> diff --git a/builtin/last-modified.c b/builtin/last-modified.c\n> index 9206bbdc1d..ccb7ff66d4 100644\n> --- a/builtin/last-modified.c\n> +++ b/builtin/last-modified.c\n> @@ -25,6 +25,7 @@\n>\n>  #define LAST_MODIFIED_INIT { \\\n>  \t.line_termination = '\\n', \\\n> +\t.max_depth = -1, \\\n>  }\n>\n>  struct last_modified_entry {\n> @@ -60,6 +61,7 @@ struct last_modified {\n>  \tbool recursive;\n>  \tbool show_trees;\n>  \tint line_termination;\n> +\tint max_depth;\n>\n\nShould this be signed?\n\n>  \tconst char **all_paths;\n>  \tsize_t all_paths_nr;\n> @@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n>  \tlm->rev.diffopt.flags.recursive = lm->recursive;\n>  \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n>\n> +\tif (lm->max_depth >= 0) {\n> +\t\tlm->rev.diffopt.flags.recursive = 1;\n> +\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n> +\t\tlm->rev.diffopt.max_depth_valid = 1;\n> +\t}\n> +\n\nOr if our goal is to actually handle them within the\n'git-last-modified(1)' command, shouldn't we ensure we don't allow any\nadditional flags from being parsed as diffopt?\n\nCurrently other diffopts flags such as '--no-prefix', '--cc' and so on,\nare parsed even if they don't affect the output of\n'git-last-modified(1)'. Shouldn't we disallow such behavior?\n\n>  \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n>  \tif (argc > 1) {\n>  \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n> @@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \tstruct last_modified lm = LAST_MODIFIED_INIT;\n>\n>  \tconst char * const last_modified_usage[] = {\n> -\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n>  \t\t   \"[<revision-range>] [[--] <path>...]\"),\n>  \t\tNULL\n>  \t};\n> @@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \t\t\t N_(\"recurse into subtrees\")),\n>  \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n>  \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n> +\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n> +\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n>  \t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n>  \t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n>  \t\tOPT_END()\n>\n> --\n> 2.51.2\n"},{"id":"531311","messageId":"xmqq3460pw8y.fsf@gitster.g","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-1-608350df0caa@iotcl.com","subject":"Re: [PATCH 1/3] last-modified: handle and document NUL termination","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T16:57:33Z","receivedAt":"2025-11-26T16:57:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> When option `-z` is provided to git-last-modified(1), each line is\n> separated with a NUL instead of a newline. Document this properly and\n> handle parsing of the option in the builtin itself.\n\nI think documenting does make sense, but it is not clear from the\ndescription why it is better to handle the option in the builtin\nitself, instead of letting the setup_revisions() take care of it.\n\nIs it because after the command lets setup_revisions() to parse out\nthe revision range, the command does not really let the revision\nmachinery drive diffs and let it output anything (hence, even though\nrev.diffopt.line_termination is set from the command line, the calling\nbuiltin is the only one that pays attention, and the revision machinery\nand the diff machinery called from there does not pay attention to it?\n\nAnd assuming that this new division of labor between the revision\nmachinery and the subcommand makes sense (it needs to be explained\nbetter), the updated code does make sense to me.\n\nBut it looks suboptimal.  See below.\n\n> +#define LAST_MODIFIED_INIT { \\\n> +\t.line_termination = '\\n', \\\n> +}\n\nYou have to introduce such a non-zero initialization, only because\nyou pretend to accept _any_ byte here, and use it as the line\ntermination character.  If you were porting Git to ancient Macintosh,\nyou could set this to '\\r' and it would follow their text file\nconvention there ;-)\n\nBut ...\n\n>  struct last_modified_entry {\n>  \tstruct hashmap_entry hashent;\n>  \tstruct object_id oid;\n> @@ -55,6 +59,7 @@ struct last_modified {\n>  \tstruct rev_info rev;\n>  \tbool recursive;\n>  \tbool show_trees;\n> +\tint line_termination;\n>  \n>  \tconst char **all_paths;\n>  \tsize_t all_paths_nr;\n> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,\n>  \t\tputchar('^');\n>  \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n>  \n> -\tif (lm->rev.diffopt.line_termination)\n> +\tif (lm->line_termination)\n>  \t\twrite_name_quoted(path, stdout, '\\n');\n>  \telse\n>  \t\tprintf(\"%s%c\", path, '\\0');\n\n... you use hardcoded '\\n' here, without allowing the value of\nline_termination to affect the termination character.\n\nThis is way suboptimal.  Instead, would it work if you add\n\n\tbool null_termination;\n\nto the last_modified structure, and do\n\n\tif (!lm->null_termination)\n\t\twrite_name_quoted(path, stdout, '\\n');\n\telse\n\t\tprintf(\"%s%c\", path, '\\0');\n\nhere?   Then\n\n> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \t\t      struct repository *repo)\n>  {\n>  \tint ret;\n> -\tstruct last_modified lm = { 0 };\n> +\tstruct last_modified lm = LAST_MODIFIED_INIT;\n\nYou do not need this change, and\n\n>  \tconst char * const last_modified_usage[] = {\n> -\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n>  \t\t   \"[<revision-range>] [[--] <path>...]\"),\n>  \t\tNULL\n>  \t};\n> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \t\t\t N_(\"recurse into subtrees\")),\n>  \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n>  \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n> +\t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n> +\t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n\nThis will become OPT_BOOL() to set the &lm.null_termination.\n\n>  \t\tOPT_END()\n>  \t};\n"},{"id":"531320","messageId":"CAPig+cTueuZZ-_51Eje3MMgtvvuDBzv-9yRU2xBfJQUaU-vFJw@mail.gmail.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-3-608350df0caa@iotcl.com","subject":"Re: [PATCH 3/3] last-modified: better document how depth in handled","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2025-11-26T17:38:18Z","receivedAt":"2025-11-26T17:38:30Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Nov 26, 2025 at 1:10 AM Toon Claes <toon@iotcl.com> wrote:\n> last-modified: better document how depth in handled\n\ns/in/is/\n\n> By default git-last-modified(1) only shows information about paths at\n> the root level. This can be confusing. Clarify the command's behavior in\n> the documentation.\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n"},{"id":"531329","messageId":"xmqqldjsmv5p.fsf@gitster.g","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-2-608350df0caa@iotcl.com","subject":"Re: [PATCH 2/3] last-modified: document option --max-depth","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-26T19:49:22Z","receivedAt":"2025-11-26T19:49:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> Option --max-depth is supported by git-last-modified(1), because it was\n> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n> max-depth parameter, 2025-08-07).\n>\n> This option is useful for everyday use of the git-last-modified(1)\n> command, so document it's existence in the man page and `-h` output.\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-last-modified.adoc |  9 ++++++++-\n>  builtin/last-modified.c              | 12 +++++++++++-\n>  2 files changed, 19 insertions(+), 2 deletions(-)\n\nDoes this step pass t0450?\n\n    fixup! last-modified: document option --max-depth\n\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex ccb7ff66d4..857554e70d 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -523,8 +523,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = LAST_MODIFIED_INIT;\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n-\t\t   \"[<revision-range>] [[--] <path>...]\"),\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\\n\"\n+\t\t   \"    [<revision-range>] [[--] <path>...]\"),\n \t\tNULL\n \t};\n \n"},{"id":"531408","messageId":"87tsye0z61.fsf@iotcl.com","threadId":"64531","inReplyTo":"xmqq3460pw8y.fsf@gitster.g","subject":"Re: [PATCH 1/3] last-modified: handle and document NUL termination","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-28T18:50:30Z","receivedAt":"2025-11-28T18:50:46Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Toon Claes <toon@iotcl.com> writes:\n>\n>> When option `-z` is provided to git-last-modified(1), each line is\n>> separated with a NUL instead of a newline. Document this properly and\n>> handle parsing of the option in the builtin itself.\n>\n> I think documenting does make sense, but it is not clear from the\n> description why it is better to handle the option in the builtin\n> itself, instead of letting the setup_revisions() take care of it.\n\nI know it's silly, but I wanted to feed these options to\nparse_options(). Doing this would make them show up in `git\nlast-modified -h`.\n\n> Is it because after the command lets setup_revisions() to parse out\n> the revision range, the command does not really let the revision\n> machinery drive diffs and let it output anything (hence, even though\n> rev.diffopt.line_termination is set from the command line, the calling\n> builtin is the only one that pays attention, and the revision machinery\n> and the diff machinery called from there does not pay attention to it?\n\nThat too, at least for this option. Not the other patch.\n\n> And assuming that this new division of labor between the revision\n> machinery and the subcommand makes sense (it needs to be explained\n> better), the updated code does make sense to me.\n>\n> But it looks suboptimal.  See below.\n>\n>> +#define LAST_MODIFIED_INIT { \\\n>> +\t.line_termination = '\\n', \\\n>> +}\n>\n> You have to introduce such a non-zero initialization, only because\n> you pretend to accept _any_ byte here, and use it as the line\n> termination character.  If you were porting Git to ancient Macintosh,\n> you could set this to '\\r' and it would follow their text file\n> convention there ;-)\n>\n> But ...\n>\n>>  struct last_modified_entry {\n>>  \tstruct hashmap_entry hashent;\n>>  \tstruct object_id oid;\n>> @@ -55,6 +59,7 @@ struct last_modified {\n>>  \tstruct rev_info rev;\n>>  \tbool recursive;\n>>  \tbool show_trees;\n>> +\tint line_termination;\n>>  \n>>  \tconst char **all_paths;\n>>  \tsize_t all_paths_nr;\n>> @@ -165,7 +170,7 @@ static void last_modified_emit(struct last_modified *lm,\n>>  \t\tputchar('^');\n>>  \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n>>  \n>> -\tif (lm->rev.diffopt.line_termination)\n>> +\tif (lm->line_termination)\n>>  \t\twrite_name_quoted(path, stdout, '\\n');\n>>  \telse\n>>  \t\tprintf(\"%s%c\", path, '\\0');\n>\n> ... you use hardcoded '\\n' here, without allowing the value of\n> line_termination to affect the termination character.\n>\n> This is way suboptimal.  Instead, would it work if you add\n>\n> \tbool null_termination;\n>\n> to the last_modified structure, and do\n>\n> \tif (!lm->null_termination)\n> \t\twrite_name_quoted(path, stdout, '\\n');\n> \telse\n> \t\tprintf(\"%s%c\", path, '\\0');\n>\n> here?   Then\n>\n>> @@ -507,10 +512,10 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>>  \t\t      struct repository *repo)\n>>  {\n>>  \tint ret;\n>> -\tstruct last_modified lm = { 0 };\n>> +\tstruct last_modified lm = LAST_MODIFIED_INIT;\n>\n> You do not need this change, and\n>\n>>  \tconst char * const last_modified_usage[] = {\n>> -\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n>> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n>>  \t\t   \"[<revision-range>] [[--] <path>...]\"),\n>>  \t\tNULL\n>>  \t};\n>> @@ -520,6 +525,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>>  \t\t\t N_(\"recurse into subtrees\")),\n>>  \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n>>  \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n>> +\t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n>> +\t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n>\n> This will become OPT_BOOL() to set the &lm.null_termination.\n>\n>>  \t\tOPT_END()\n>>  \t};\n>\n\nI actually agree this is better. But I was following the pattern of\n`line_termination` other in various other places. I'll adapt it to use a\nbool instead.\n\n-- \nCheers,\nToon\n"},{"id":"531409","messageId":"87qzti0z3s.fsf@iotcl.com","threadId":"64531","inReplyTo":"xmqqldjsmv5p.fsf@gitster.g","subject":"Re: [PATCH 2/3] last-modified: document option --max-depth","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-11-28T18:51:51Z","receivedAt":"2025-11-28T18:52:13Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Toon Claes <toon@iotcl.com> writes:\n>\n>> Option --max-depth is supported by git-last-modified(1), because it was\n>> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n>> max-depth parameter, 2025-08-07).\n>>\n>> This option is useful for everyday use of the git-last-modified(1)\n>> command, so document it's existence in the man page and `-h` output.\n>>\n>> Signed-off-by: Toon Claes <toon@iotcl.com>\n>> ---\n>>  Documentation/git-last-modified.adoc |  9 ++++++++-\n>>  builtin/last-modified.c              | 12 +++++++++++-\n>>  2 files changed, 19 insertions(+), 2 deletions(-)\n>\n> Does this step pass t0450?\n>\n>     fixup! last-modified: document option --max-depth\n>\n> diff --git a/builtin/last-modified.c b/builtin/last-modified.c\n> index ccb7ff66d4..857554e70d 100644\n> --- a/builtin/last-modified.c\n> +++ b/builtin/last-modified.c\n> @@ -523,8 +523,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>  \tstruct last_modified lm = LAST_MODIFIED_INIT;\n>  \n>  \tconst char * const last_modified_usage[] = {\n> -\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n> -\t\t   \"[<revision-range>] [[--] <path>...]\"),\n> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\\n\"\n> +\t\t   \"    [<revision-range>] [[--] <path>...]\"),\n>  \t\tNULL\n>  \t};\n\nDerp, I should have checked that. I knew it could be a problem, but by\nthe time I was about to send the patch I forgot. Sorry about that.\n\n-- \nCheers,\nToon\n"},{"id":"531496","messageId":"aS1uxbvNE6rAQ1dl@pks.im","threadId":"64531","inReplyTo":"87tsye0z61.fsf@iotcl.com","subject":"Re: [PATCH 1/3] last-modified: handle and document NUL termination","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-01T10:32:37Z","receivedAt":"2025-12-01T10:32:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Nov 28, 2025 at 07:50:30PM +0100, Toon Claes wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Toon Claes <toon@iotcl.com> writes:\n> >\n> >> When option `-z` is provided to git-last-modified(1), each line is\n> >> separated with a NUL instead of a newline. Document this properly and\n> >> handle parsing of the option in the builtin itself.\n> >\n> > I think documenting does make sense, but it is not clear from the\n> > description why it is better to handle the option in the builtin\n> > itself, instead of letting the setup_revisions() take care of it.\n> \n> I know it's silly, but I wanted to feed these options to\n> parse_options(). Doing this would make them show up in `git\n> last-modified -h`.\n\nI think that reasoning makes sense, but it's certainly non-obvious from\nthe commit message. So if you expand the commit message with an\nexplanation the change becomes much more sensible.\n\nPatrick\n"},{"id":"531497","messageId":"aS1uz6mc0WW9kjzN@pks.im","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-3-608350df0caa@iotcl.com","subject":"Re: [PATCH 3/3] last-modified: better document how depth in handled","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-01T10:32:47Z","receivedAt":"2025-12-01T10:32:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 26, 2025 at 07:09:45AM +0100, Toon Claes wrote:\n> By default git-last-modified(1) only shows information about paths at\n> the root level. This can be confusing. Clarify the command's behavior in\n> the documentation.\n\nHm, that's confusing indeed. Is it possible for git-last-modified(1) to\ndo the \"right thing\" automatically? That is, given \"sub/file\", show when\nthat specific file has been last modified? Or is there a good\n(non-technical) reason it behaves the way it does?\n\nPatrick\n"},{"id":"531550","messageId":"87tsy9w3k1.fsf@iotcl.com","threadId":"64531","inReplyTo":"aS1uz6mc0WW9kjzN@pks.im","subject":"Re: [PATCH 3/3] last-modified: better document how depth in handled","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-12-02T11:01:18Z","receivedAt":"2025-12-02T11:01:32Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hm, that's confusing indeed. Is it possible for git-last-modified(1) to\n> do the \"right thing\" automatically? That is, given \"sub/file\", show when\n> that specific file has been last modified? Or is there a good\n> (non-technical) reason it behaves the way it does?\n\nYou bring up a good point. In the version of git-blame-tree(1) that\nGitHub did share, the `--recursive` flag is enabled by default. So if\nyou pass a file path to the command, you'll get the \"right thing\". But\nas I am pointing out in this patch, if you pass a subtree, everything in\nthat subtree is shown too. You could argue this is the \"right thing\".\n\nAnyhow, in the version of git-last-modified(1) I submitted upstream,\nrecursive is not enabled by default. My reason, at the time option\n--max-depth wasn't implemented yet. I submitted those changes in a\nseparate series (these patches also originate from GitHub by the way).\nIf those patches wouldn't land, I think always-on recursive behavior for\ngit-last-modified(1) would be quite annoying.\n\nSo long story short, as git-last-modified(1) is still marked as\n\"EXPERIMENTAL\", shall we make recursive always-on?\n\n-- \nCheers,\nToon\n"},{"id":"531566","messageId":"aS8eesW53ks_4BSt@pks.im","threadId":"64531","inReplyTo":"87tsy9w3k1.fsf@iotcl.com","subject":"Re: [PATCH 3/3] last-modified: better document how depth in handled","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T17:14:34Z","receivedAt":"2025-12-02T17:14:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Dec 02, 2025 at 12:01:18PM +0100, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Hm, that's confusing indeed. Is it possible for git-last-modified(1) to\n> > do the \"right thing\" automatically? That is, given \"sub/file\", show when\n> > that specific file has been last modified? Or is there a good\n> > (non-technical) reason it behaves the way it does?\n> \n> You bring up a good point. In the version of git-blame-tree(1) that\n> GitHub did share, the `--recursive` flag is enabled by default. So if\n> you pass a file path to the command, you'll get the \"right thing\". But\n> as I am pointing out in this patch, if you pass a subtree, everything in\n> that subtree is shown too. You could argue this is the \"right thing\".\n> \n> Anyhow, in the version of git-last-modified(1) I submitted upstream,\n> recursive is not enabled by default. My reason, at the time option\n> --max-depth wasn't implemented yet. I submitted those changes in a\n> separate series (these patches also originate from GitHub by the way).\n> If those patches wouldn't land, I think always-on recursive behavior for\n> git-last-modified(1) would be quite annoying.\n> \n> So long story short, as git-last-modified(1) is still marked as\n> \"EXPERIMENTAL\", shall we make recursive always-on?\n\nI think that is a good idea, as it sounds like it would make common use\ncases way more intuitive.\n\nAn alternative would be to default to a max-depth of 0 and stick to the\nnon-recursive default. In that case, the command would work intuitively\nto the user, right? At least it would work intuitively if the user\ndoesn't want a recursive listing.\n\nSo maybe we should combine these two improvements. That is, we use:\n\n  - An infinite max-depth by default with the recursive bahviour.\n\n  - A max-depth of 0 in the non-recursive case.\n\nIn this case, we'd always recursively list all entries by default, but\nin case the user explicitly doesn't want recursive behaviour we'd know\nto show the last modification date of all provided files.\n\nI was briefly thinking that a max-depth of 1 might be a more obvious\ndefault for the non-recursive case. In that case, doing e.g. `git\nlast-modified --no-recursive t` would list the contents of \"t/\", but\nnothing more. I'm a bit torn there though whether that really is a\nsufficient improvement over a max-depth of 0.\n\nPatrick\n"},{"id":"534029","messageId":"87fr85kb5u.fsf@iotcl.com","threadId":"64531","inReplyTo":"CAOLa=ZSbV6SeJ9orOz0T+oh3PVhYhTsaxrsYVkr+5q7i_tsVCw@mail.gmail.com","subject":"Re: [PATCH 2/3] last-modified: document option --max-depth","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T12:13:49Z","receivedAt":"2026-01-16T12:14:01Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Toon Claes <toon@iotcl.com> writes:\n>\n>> Option --max-depth is supported by git-last-modified(1), because it was\n>> added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n>> max-depth parameter, 2025-08-07).\n>>\n>\n> At this point, does it make more sense to link the respective sections\n> within 'Documentation/diff-options.adoc' as done by many other commands?\n> This would ensure that we don't have to repeat the documentation.\n\nNah, there are way too many options that are not relevant for this\ncommand in there. I agree, it's annoying to duplicate things. But I\ndon't know there's an easy way around that right now.\n\n>> This option is useful for everyday use of the git-last-modified(1)\n>> command, so document it's existence in the man page and `-h` output.\n>>\n>> Signed-off-by: Toon Claes <toon@iotcl.com>\n>> ---\n>>  Documentation/git-last-modified.adoc |  9 ++++++++-\n>>  builtin/last-modified.c              | 12 +++++++++++-\n>>  2 files changed, 19 insertions(+), 2 deletions(-)\n>>\n>> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\n>> index cd4a5040b0..8409daebe9 100644\n>> --- a/Documentation/git-last-modified.adoc\n>> +++ b/Documentation/git-last-modified.adoc\n>> @@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n>>  SYNOPSIS\n>>  --------\n>>  [synopsis]\n>> -git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n>> +git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n>> +\t[<revision-range>] [[--] <path>...]\n>>\n>>  DESCRIPTION\n>>  -----------\n>> @@ -32,6 +33,12 @@ OPTIONS\n>>  \tShow tree entries even when recursing into them. It has no effect\n>>  \twithout `--recursive`.\n>>\n>> +`--max-depth=<depth>`::\n>> +\tFor each pathspec given on the command line, descend at most `<depth>`\n>> +\tlevels of directories. A negative value means no limit.\n>> +\tSetting a positive value implies `--recursive`.\n>> +\tCannot be combined with wildcards in the pathspec.\n>> +\n>>  `-z`::\n>>  \tTerminate each line with a _NUL_ rather than a newline.\n>>\n>> diff --git a/builtin/last-modified.c b/builtin/last-modified.c\n>> index 9206bbdc1d..ccb7ff66d4 100644\n>> --- a/builtin/last-modified.c\n>> +++ b/builtin/last-modified.c\n>> @@ -25,6 +25,7 @@\n>>\n>>  #define LAST_MODIFIED_INIT { \\\n>>  \t.line_termination = '\\n', \\\n>> +\t.max_depth = -1, \\\n>>  }\n>>\n>>  struct last_modified_entry {\n>> @@ -60,6 +61,7 @@ struct last_modified {\n>>  \tbool recursive;\n>>  \tbool show_trees;\n>>  \tint line_termination;\n>> +\tint max_depth;\n>>\n>\n> Should this be signed?\n\nYes, a negative number is allowed to set an unlimited max depth.\n\n>\n>>  \tconst char **all_paths;\n>>  \tsize_t all_paths_nr;\n>> @@ -487,6 +489,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n>>  \tlm->rev.diffopt.flags.recursive = lm->recursive;\n>>  \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n>>\n>> +\tif (lm->max_depth >= 0) {\n>> +\t\tlm->rev.diffopt.flags.recursive = 1;\n>> +\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n>> +\t\tlm->rev.diffopt.max_depth_valid = 1;\n>> +\t}\n>> +\n>\n> Or if our goal is to actually handle them within the\n> 'git-last-modified(1)' command, shouldn't we ensure we don't allow any\n> additional flags from being parsed as diffopt?\n\nIs that possible?\n\nAll the magic that happens in setup_revision() is a little bit of a pain\nif you ask me. \n\n> Currently other diffopts flags such as '--no-prefix', '--cc' and so on,\n> are parsed even if they don't affect the output of\n> 'git-last-modified(1)'. Shouldn't we disallow such behavior?\n\nI think this would require a revamp of setup_revision(). But I'm happy\nto be proven otherwise.\n\n>>  \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n>>  \tif (argc > 1) {\n>>  \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n>> @@ -515,7 +523,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>>  \tstruct last_modified lm = LAST_MODIFIED_INIT;\n>>\n>>  \tconst char * const last_modified_usage[] = {\n>> -\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n>> +\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n>>  \t\t   \"[<revision-range>] [[--] <path>...]\"),\n>>  \t\tNULL\n>>  \t};\n>> @@ -525,6 +533,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n>>  \t\t\t N_(\"recurse into subtrees\")),\n>>  \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n>>  \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n>> +\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n>> +\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n>>  \t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n>>  \t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n>>  \t\tOPT_END()\n>>\n>> --\n>> 2.51.2\n\n-- \nCheers,\nToon\n"},{"id":"534037","messageId":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com","subject":"[PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:48Z","receivedAt":"2026-01-16T13:23:01Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"We have had several reports[1][2] from users that have been thoroughly\nconfused by the default behaviour of git-last-modified(1). Most\nimportantly, when using the command on trees other than the root tree\nthe requests are seemingly ignoring that request and instead blame the\ntop-level subtree. For example:\n\n    $ git last-modified t/lib-bash.sh\n    acdfea4394db8e8b42f48e36f7726d64a909a89d t\n\nThis result is completely unexpected and feels buggy to anybody who is\nnot deeply familiar with git-last-modified(1). Furthermore, users\ntypically have a hard time to figure out the correct parameters.\n\nThis series changes the behavior to something that intuitively makes\nmore sense.\n\nThe change of the default behavior is done in the last commit, in the\nlead-up commits changes in the documentation are made.\n\n[1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com>\n[2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>\n\n---\nChanges in v2:\n- Change the default --max-depth.\n- Split up commits in smaller pieces.\n- Add more testss\n- Link to v1: https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com\n\nCc: \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com>\nCc: Gusted <gusted@codeberg.org>\n\n---\nToon Claes (5):\n      last-modified: document NUL termination\n      last-modified: add option '-z' to help output\n      last-modified: document option --max-depth\n      last-modified: add option '--max-depth' to help output\n      last-modified: change default max-depth to 0\n\n Documentation/git-last-modified.adoc | 29 ++++++++++++++++++++++++++++-\n builtin/last-modified.c              | 20 ++++++++++++++------\n t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++\n 3 files changed, 77 insertions(+), 7 deletions(-)\n\nRange-diff versus v1:\n\n1:  7f599cf0d1 ! 1:  4a9b8170c3 last-modified: handle and document NUL termination\n    @@ Metadata\n     Author: Toon Claes <toon@iotcl.com>\n     \n      ## Commit message ##\n    -    last-modified: handle and document NUL termination\n    +    last-modified: document NUL termination\n     \n    -    When option `-z` is provided to git-last-modified(1), each line is\n    -    separated with a NUL instead of a newline. Document this properly and\n    -    handle parsing of the option in the builtin itself.\n    +    The command git-last-modified(1) already recognizes the option '-z', and\n    +    similar to many other commands this will make the output NUL-terminated\n    +    instead of using newlines. Although, this option is missing from the\n    +    documentation, so add it.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    @@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show whe\n      --------\n      [synopsis]\n     -git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n    -+git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n    ++git last-modified [--recursive] [--show-trees] [-z]\n    ++\t\t  [<revision-range>] [[--] <path>...]\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-last-modified.adoc: OPTIONS\n      \twithout `--recursive`.\n      \n     +`-z`::\n    -+\tTerminate each line with a _NUL_ rather than a newline.\n    ++\tTerminate each line with a _NUL_ character rather than a newline.\n     +\n      `<revision-range>`::\n      \tOnly traverse commits in the specified revision range. When no\n    @@ Documentation/git-last-modified.adoc: OPTIONS\n      linkgit:git-blame[1],\n     \n      ## builtin/last-modified.c ##\n    -@@\n    - #define PARENT1 (1u<<16) /* used instead of SEEN */\n    - #define PARENT2 (1u<<17) /* used instead of BOTTOM, BOUNDARY */\n    - \n    -+#define LAST_MODIFIED_INIT { \\\n    -+\t.line_termination = '\\n', \\\n    -+}\n    -+\n    - struct last_modified_entry {\n    - \tstruct hashmap_entry hashent;\n    - \tstruct object_id oid;\n    -@@ builtin/last-modified.c: struct last_modified {\n    - \tstruct rev_info rev;\n    - \tbool recursive;\n    - \tbool show_trees;\n    -+\tint line_termination;\n    - \n    - \tconst char **all_paths;\n    - \tsize_t all_paths_nr;\n    -@@ builtin/last-modified.c: static void last_modified_emit(struct last_modified *lm,\n    - \t\tputchar('^');\n    - \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n    - \n    --\tif (lm->rev.diffopt.line_termination)\n    -+\tif (lm->line_termination)\n    - \t\twrite_name_quoted(path, stdout, '\\n');\n    - \telse\n    - \t\tprintf(\"%s%c\", path, '\\0');\n     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    - \t\t      struct repository *repo)\n    - {\n    - \tint ret;\n    --\tstruct last_modified lm = { 0 };\n    -+\tstruct last_modified lm = LAST_MODIFIED_INIT;\n    + \tstruct last_modified lm = { 0 };\n      \n      \tconst char * const last_modified_usage[] = {\n     -\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n    -+\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n    - \t\t   \"[<revision-range>] [[--] <path>...]\"),\n    +-\t\t   \"[<revision-range>] [[--] <path>...]\"),\n    ++\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n    ++\t\t   \"                  [<revision-range>] [[--] <path>...]\"),\n      \t\tNULL\n      \t};\n    -@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    - \t\t\t N_(\"recurse into subtrees\")),\n    - \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n    - \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n    -+\t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n    -+\t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n    - \t\tOPT_END()\n    - \t};\n      \n-:  ---------- > 2:  94efeb29b1 last-modified: add option '-z' to help output\n-:  ---------- > 3:  5669ab25e8 last-modified: document option --max-depth\n2:  f193241248 ! 4:  b3060f8b38 last-modified: document option --max-depth\n    @@ Metadata\n     Author: Toon Claes <toon@iotcl.com>\n     \n      ## Commit message ##\n    -    last-modified: document option --max-depth\n    +    last-modified: add option '--max-depth' to help output\n     \n    -    Option --max-depth is supported by git-last-modified(1), because it was\n    -    added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n    -    max-depth parameter, 2025-08-07).\n    +    In previous commit option '--max-depth' was added to the documentation.\n    +    To have it also appear in the help output of `git last-modified -h`,\n    +    move the handling of '--max-depth' to parse_options() in\n    +    builtin/last-modified.c itself.\n     \n    -    This option is useful for everyday use of the git-last-modified(1)\n    -    command, so document it's existence in the man page and `-h` output.\n    +    It enables us to change default behavior in a subsequent commit.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    - ## Documentation/git-last-modified.adoc ##\n    -@@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show when files were last modified\n    - SYNOPSIS\n    - --------\n    - [synopsis]\n    --git last-modified [--recursive] [--show-trees] [-z] [<revision-range>] [[--] <path>...]\n    -+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n    -+\t[<revision-range>] [[--] <path>...]\n    - \n    - DESCRIPTION\n    - -----------\n    -@@ Documentation/git-last-modified.adoc: OPTIONS\n    - \tShow tree entries even when recursing into them. It has no effect\n    - \twithout `--recursive`.\n    - \n    -+`--max-depth=<depth>`::\n    -+\tFor each pathspec given on the command line, descend at most `<depth>`\n    -+\tlevels of directories. A negative value means no limit.\n    -+\tSetting a positive value implies `--recursive`.\n    -+\tCannot be combined with wildcards in the pathspec.\n    -+\n    - `-z`::\n    - \tTerminate each line with a _NUL_ rather than a newline.\n    - \n    -\n      ## builtin/last-modified.c ##\n    -@@\n    - \n    - #define LAST_MODIFIED_INIT { \\\n    - \t.line_termination = '\\n', \\\n    -+\t.max_depth = -1, \\\n    - }\n    - \n    - struct last_modified_entry {\n     @@ builtin/last-modified.c: struct last_modified {\n      \tbool recursive;\n      \tbool show_trees;\n    - \tint line_termination;\n    + \tbool null_termination;\n     +\tint max_depth;\n      \n      \tconst char **all_paths;\n    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,\n      \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n      \tif (argc > 1) {\n      \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n    -@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    - \tstruct last_modified lm = LAST_MODIFIED_INIT;\n    - \n    - \tconst char * const last_modified_usage[] = {\n    --\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z] \"\n    -+\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z] \"\n    - \t\t   \"[<revision-range>] [[--] <path>...]\"),\n    - \t\tNULL\n    - \t};\n     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n      \t\t\t N_(\"recurse into subtrees\")),\n      \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n      \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n     +\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n     +\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n    - \t\tOPT_SET_INT('z', NULL, &lm.line_termination,\n    - \t\t\tN_(\"lines are separated with NUL character\"), '\\0'),\n    + \t\tOPT_BOOL('z', NULL, &lm.null_termination,\n    + \t\t\tN_(\"lines are separated with NUL character\")),\n      \t\tOPT_END()\n    + \t};\n    + \n    ++\t/*\n    ++\t * Set the default of a max-depth to \"unset\". This will change in a\n    ++\t * subsequent commit.\n    ++\t */\n    ++\tlm.max_depth = -1;\n    ++\n    + \targc = parse_options(argc, argv, prefix, last_modified_options,\n    + \t\t\t     last_modified_usage,\n    + \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n3:  6773f073d6 < -:  ---------- last-modified: better document how depth in handled\n-:  ---------- > 5:  41819f8732 last-modified: change default max-depth to 0\n\n\n---\nbase-commit: 7264e61d87e58b9d0f5e6424c47c11e9657dfb75\nchange-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4\n\n"},{"id":"534038","messageId":"20260116-toon-last-modified-zzzz-v2-1-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v2 1/5] last-modified: document NUL termination","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:49Z","receivedAt":"2026-01-16T13:23:09Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The command git-last-modified(1) already recognizes the option '-z', and\nsimilar to many other commands this will make the output NUL-terminated\ninstead of using newlines. Although, this option is missing from the\ndocumentation, so add it.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 22 +++++++++++++++++++++-\n builtin/last-modified.c              |  4 ++--\n 2 files changed, 23 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 602843e095..2e5f370c15 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n+git last-modified [--recursive] [--show-trees] [-z]\n+\t\t  [<revision-range>] [[--] <path>...]\n \n DESCRIPTION\n -----------\n@@ -32,6 +33,9 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`-z`::\n+\tTerminate each line with a _NUL_ character rather than a newline.\n+\n `<revision-range>`::\n \tOnly traverse commits in the specified revision range. When no\n \t`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole\n@@ -44,6 +48,22 @@ OPTIONS\n \tWithout an optional path parameter, all files and subdirectories\n \tin path traversal the are included in the output.\n \n+OUTPUT\n+------\n+\n+The output is in the format:\n+\n+------------\n+ <oid> TAB <path> LF\n+------------\n+\n+If a path contains any special characters, the path is C-style quoted. To\n+avoid quoting, pass option `-z` to terminate each line with a NUL.\n+\n+------------\n+ <oid> TAB <path> NUL\n+------------\n+\n SEE ALSO\n --------\n linkgit:git-blame[1],\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex c80f0535f6..cac66d03fd 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -510,8 +510,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = { 0 };\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n-\t\t   \"[<revision-range>] [[--] <path>...]\"),\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n+\t\t   \"                  [<revision-range>] [[--] <path>...]\"),\n \t\tNULL\n \t};\n \n\n-- \n2.52.0\n\n"},{"id":"534039","messageId":"20260116-toon-last-modified-zzzz-v2-2-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v2 2/5] last-modified: add option '-z' to help output","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:50Z","receivedAt":"2026-01-16T13:23:13Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The parsing of option '-z' is done by diff_opt_parse(), which is called\nby setup_revisions(), and ends up filling in\n`struct diff_options::line_termination`. But that field isn't used by\nthe diff machinery itself, only by builtin/last-modified.c to format\nthe output.\n\nTo have '-z' also appear in the help output of `git last-modified -h`,\nmove the handling of '-z' to parse_options() in builtin/last-modified.c\nitself.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/last-modified.c | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex cac66d03fd..0d73384e45 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -55,6 +55,7 @@ struct last_modified {\n \tstruct rev_info rev;\n \tbool recursive;\n \tbool show_trees;\n+\tbool null_termination;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -165,10 +166,10 @@ static void last_modified_emit(struct last_modified *lm,\n \t\tputchar('^');\n \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n \n-\tif (lm->rev.diffopt.line_termination)\n-\t\twrite_name_quoted(path, stdout, '\\n');\n-\telse\n+\tif (lm->null_termination)\n \t\tprintf(\"%s%c\", path, '\\0');\n+\telse\n+\t\twrite_name_quoted(path, stdout, '\\n');\n }\n \n static void mark_path(const char *path, const struct object_id *oid,\n@@ -520,6 +521,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_BOOL('z', NULL, &lm.null_termination,\n+\t\t\tN_(\"lines are separated with NUL character\")),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.52.0\n\n"},{"id":"534040","messageId":"20260116-toon-last-modified-zzzz-v2-3-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v2 3/5] last-modified: document option --max-depth","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:51Z","receivedAt":"2026-01-16T13:23:18Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --max-depth is supported by git-last-modified(1), because it was\nadded to the diff machinery in a1dfa5448d (diff: teach tree-diff a\nmax-depth parameter, 2025-08-07).\n\nThis option is useful for everyday use of the git-last-modified(1)\ncommand, so document it's existence in the man page.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 8 +++++++-\n builtin/last-modified.c              | 2 +-\n 2 files changed, 8 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 2e5f370c15..a3992db3f2 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [-z]\n+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n \t\t  [<revision-range>] [[--] <path>...]\n \n DESCRIPTION\n@@ -33,6 +33,12 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`--max-depth=<depth>`::\n+\tFor each pathspec given on the command line, descend at most `<depth>`\n+\tlevels of directories. A negative value means no limit.\n+\tSetting a positive value implies `--recursive`.\n+\tCannot be combined with wildcards in the pathspec.\n+\n `-z`::\n \tTerminate each line with a _NUL_ character rather than a newline.\n \ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 0d73384e45..324363b5ca 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -511,7 +511,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = { 0 };\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\\n\"\n \t\t   \"                  [<revision-range>] [[--] <path>...]\"),\n \t\tNULL\n \t};\n\n-- \n2.52.0\n\n"},{"id":"534041","messageId":"20260116-toon-last-modified-zzzz-v2-4-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v2 4/5] last-modified: add option '--max-depth' to help output","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:52Z","receivedAt":"2026-01-16T13:23:24Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In previous commit option '--max-depth' was added to the documentation.\nTo have it also appear in the help output of `git last-modified -h`,\nmove the handling of '--max-depth' to parse_options() in\nbuiltin/last-modified.c itself.\n\nIt enables us to change default behavior in a subsequent commit.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/last-modified.c | 15 +++++++++++++++\n 1 file changed, 15 insertions(+)\n\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 324363b5ca..842700bc6a 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -56,6 +56,7 @@ struct last_modified {\n \tbool recursive;\n \tbool show_trees;\n \tbool null_termination;\n+\tint max_depth;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -483,6 +484,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n \tlm->rev.diffopt.flags.recursive = lm->recursive;\n \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n \n+\tif (lm->max_depth >= 0) {\n+\t\tlm->rev.diffopt.flags.recursive = 1;\n+\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n+\t\tlm->rev.diffopt.max_depth_valid = 1;\n+\t}\n+\n \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n \tif (argc > 1) {\n \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n@@ -521,11 +528,19 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n+\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n \t\tOPT_BOOL('z', NULL, &lm.null_termination,\n \t\t\tN_(\"lines are separated with NUL character\")),\n \t\tOPT_END()\n \t};\n \n+\t/*\n+\t * Set the default of a max-depth to \"unset\". This will change in a\n+\t * subsequent commit.\n+\t */\n+\tlm.max_depth = -1;\n+\n \targc = parse_options(argc, argv, prefix, last_modified_options,\n \t\t\t     last_modified_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n\n-- \n2.52.0\n\n"},{"id":"534042","messageId":"20260116-toon-last-modified-zzzz-v2-5-79e44f2806fe@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v2 5/5] last-modified: change default max-depth to 0","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-16T13:22:53Z","receivedAt":"2026-01-16T13:23:27Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"By default git-last-modified(1) doesn't recurse into subtrees. So when\nthe pathspec contained a path in a subtree, the command would only print\nthe commit information about the parent tree of the path, like:\n\n    $ git last-modified -- path/file\n    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath\n\nChange the default behavior to give commit information about the exact\npath instead:\n\n    $ git last-modified -- path/file\n    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath/file\n\nTo achieve this, the default max-depth is changed to 0 and recursive is\nalways enabled.\n\nThe handling of option '-r' is modified to disable a max-depth,\nresulting in the behavior of this option to remain unchanged.\n\nNo existing tests were modified, because there didn't exist any tests\ncovering the example above. But more tests are added to cover this now.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc |  3 ++-\n builtin/last-modified.c              | 16 +++-------------\n t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++\n 3 files changed, 40 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex a3992db3f2..57136baf3b 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -27,6 +27,7 @@ OPTIONS\n `--recursive`::\n \tInstead of showing tree entries, step into subtrees and show all entries\n \tinside them recursively.\n+\tThis is identical as setting `--max-depth=-1`.\n \n `-t`::\n `--show-trees`::\n@@ -36,7 +37,7 @@ OPTIONS\n `--max-depth=<depth>`::\n \tFor each pathspec given on the command line, descend at most `<depth>`\n \tlevels of directories. A negative value means no limit.\n-\tSetting a positive value implies `--recursive`.\n+\tThe default depth is 0.\n \tCannot be combined with wildcards in the pathspec.\n \n `-z`::\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 842700bc6a..a10e711beb 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -481,14 +481,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n \tlm->rev.no_commit_id = 1;\n \tlm->rev.diff = 1;\n \tlm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;\n-\tlm->rev.diffopt.flags.recursive = lm->recursive;\n+\tlm->rev.diffopt.flags.recursive = 1;\n \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n-\n-\tif (lm->max_depth >= 0) {\n-\t\tlm->rev.diffopt.flags.recursive = 1;\n-\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n-\t\tlm->rev.diffopt.max_depth_valid = 1;\n-\t}\n+\tlm->rev.diffopt.max_depth = lm->max_depth;\n+\tlm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;\n \n \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n \tif (argc > 1) {\n@@ -535,12 +531,6 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\tOPT_END()\n \t};\n \n-\t/*\n-\t * Set the default of a max-depth to \"unset\". This will change in a\n-\t * subsequent commit.\n-\t */\n-\tlm.max_depth = -1;\n-\n \targc = parse_options(argc, argv, prefix, last_modified_options,\n \t\t\t     last_modified_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\ndiff --git a/t/t8020-last-modified.sh b/t/t8020-last-modified.sh\nindex 50f4312f71..3944d2e153 100755\n--- a/t/t8020-last-modified.sh\n+++ b/t/t8020-last-modified.sh\n@@ -93,6 +93,41 @@ test_expect_success 'last-modified subdir recursive' '\n \tEOF\n '\n \n+test_expect_success 'last-modified subdir non-recursive' '\n+\tcheck_last_modified a <<-\\EOF\n+\t3 a\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified path in subdir non-recursive' '\n+\tcheck_last_modified a/file <<-\\EOF\n+\t2 a/file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified subdir with wildcard non-recursive' '\n+\tcheck_last_modified a/* <<-\\EOF\n+\t3 a/b\n+\t2 a/file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified with negative max-depth' '\n+\tcheck_last_modified --max-depth=-1 <<-\\EOF\n+\t3 a/b/file\n+\t2 a/file\n+\t1 file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified with max-depth of 1' '\n+\tcheck_last_modified --max-depth=1 <<-\\EOF\n+\t3 a/b\n+\t2 a/file\n+\t1 file\n+\tEOF\n+'\n+\n test_expect_success 'last-modified from non-HEAD commit' '\n \tcheck_last_modified HEAD^ <<-\\EOF\n \t2 a\n\n-- \n2.52.0\n\n"},{"id":"534043","messageId":"72aabd0f-bafa-4013-99b7-f065e8977f8c@app.fastmail.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"Re: [PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-16T13:34:49Z","receivedAt":"2026-01-16T13:35:10Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, Jan 16, 2026, at 14:22, Toon Claes wrote:\n> We have had several reports[1][2] from users that have been thoroughly\n> confused by the default behaviour of git-last-modified(1). Most\n> importantly, when using the command on trees other than the root tree\n> the requests are seemingly ignoring that request and instead blame the\n> top-level subtree. For example:\n>\n>     $ git last-modified t/lib-bash.sh\n>     acdfea4394db8e8b42f48e36f7726d64a909a89d t\n>\n> This result is completely unexpected and feels buggy to anybody who is\n> not deeply familiar with git-last-modified(1). Furthermore, users\n> typically have a hard time to figure out the correct parameters.\n>\n> This series changes the behavior to something that intuitively makes\n> more sense.\n>\n> The change of the default behavior is done in the last commit, in the\n> lead-up commits changes in the documentation are made.\n>\n> [1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com>\n> [2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>\n>\n> ---\n> Changes in v2:\n> - Change the default --max-depth.\n> - Split up commits in smaller pieces.\n> - Add more testss\n> - Link to v1:\n> https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com\n>\n> Cc: \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com>\n> Cc: Gusted <gusted@codeberg.org>\n\nI think the Cc list got mixed up.\n\n>\n> ---\n>[snip]\n"},{"id":"534070","messageId":"xmqqcy398l5a.fsf@gitster.g","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-2-79e44f2806fe@iotcl.com","subject":"Re: [PATCH v2 2/5] last-modified: add option '-z' to help output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-16T18:31:13Z","receivedAt":"2026-01-16T18:31:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> +\t\tOPT_BOOL('z', NULL, &lm.null_termination,\n> +\t\t\tN_(\"lines are separated with NUL character\")),\n\nAs this is a name of a file-scope static thing, I do not think it\nmatters too much, but \"&lm.null_termination\" (instead of\n\"nul_termination\") that controls use of \"NUL character\" somewhat\nbothers me ;-).\n"},{"id":"534071","messageId":"xmqq8qdx8km3.fsf@gitster.g","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-4-79e44f2806fe@iotcl.com","subject":"Re: [PATCH v2 4/5] last-modified: add option '--max-depth' to help output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-16T18:42:44Z","receivedAt":"2026-01-16T18:42:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> In previous commit option '--max-depth' was added to the documentation.\n> To have it also appear in the help output of `git last-modified -h`,\n> move the handling of '--max-depth' to parse_options() in\n> builtin/last-modified.c itself.\n>\n> It enables us to change default behavior in a subsequent commit.\n\nThis split of the max-depth into two steps does not look right to\nme.  If we are handling the option ourselves here, shouldn't we do\nso in the same step, so that \"git cmd -h\" and the synopsis section\nof the documentation match?\n"},{"id":"534073","messageId":"xmqq1pjp8k11.fsf@gitster.g","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-5-79e44f2806fe@iotcl.com","subject":"Re: [PATCH v2 5/5] last-modified: change default max-depth to 0","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-16T18:55:22Z","receivedAt":"2026-01-16T18:55:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> diff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\n> index a3992db3f2..57136baf3b 100644\n> --- a/Documentation/git-last-modified.adoc\n> +++ b/Documentation/git-last-modified.adoc\n> @@ -27,6 +27,7 @@ OPTIONS\n>  `--recursive`::\n>  \tInstead of showing tree entries, step into subtrees and show all entries\n>  \tinside them recursively.\n> +\tThis is identical as setting `--max-depth=-1`.\n\nWhen I heard that the default value of max-depth will be 0, the\nfirst thing I wondered was \"how would I spell unlimited in the new\nworld order?\", and the documentation for \"--max-depth\", not\n\"--recursive\", would have been the place I expected to fish for\nnecessary information.\n\nOver there, there is \"A negative value means no limit\", so saying\n\"identical as setting --max-depth to a negative value\" here would\nmatch the description over there better, or the user will be left\nwonderign if \"-1\" is merely an example that is negative, or if it is\nmore special than other negative values and if so in what way.\n\n> @@ -36,7 +37,7 @@ OPTIONS\n>  `--max-depth=<depth>`::\n>  \tFor each pathspec given on the command line, descend at most `<depth>`\n>  \tlevels of directories. A negative value means no limit.\n> -\tSetting a positive value implies `--recursive`.\n> +\tThe default depth is 0.\n>  \tCannot be combined with wildcards in the pathspec.\n\n> diff --git a/builtin/last-modified.c b/builtin/last-modified.c\n> index 842700bc6a..a10e711beb 100644\n> --- a/builtin/last-modified.c\n> +++ b/builtin/last-modified.c\n> @@ -481,14 +481,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n>  \tlm->rev.no_commit_id = 1;\n>  \tlm->rev.diff = 1;\n>  \tlm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;\n> -\tlm->rev.diffopt.flags.recursive = lm->recursive;\n> +\tlm->rev.diffopt.flags.recursive = 1;\n\nHmph, so this will always be recursive?\n\n>  \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n> +\tlm->rev.diffopt.max_depth = lm->max_depth;\n> +\tlm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;\n\nNot saying --recursive would keep lm->recursive==0 and non-negative\nvalue of --max-depth will flip max_depth_valid on.  Saying\n\"--recursive\" or giving a negative \"--max-value\" would make\nmax_depth_valid false, and it allows traversal all the way down to\nleaves.\n\nIt may be correct, but feels quite convoluted.  I wonder if we can\nget rid of lm->recursive altogether now as a clean-up, and have\n\"--recursive\" truly do what the documentation claimed is equivalent\nearlier, i.e. OPT_SET_INT(0, \"recursive\", &lm.max_depth, -1).  Would\nthat simplify the logic a bit and make it easier to reason about the\nlogic around here, I wonder?\n\n"},{"id":"534228","messageId":"877btck1gx.fsf@iotcl.com","threadId":"64531","inReplyTo":"72aabd0f-bafa-4013-99b7-f065e8977f8c@app.fastmail.com","subject":"Re: [PATCH v2 0/5] Change git-last-modified(1) default behavior and add documentation","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T10:44:30Z","receivedAt":"2026-01-20T10:44:50Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> On Fri, Jan 16, 2026, at 14:22, Toon Claes wrote:\n>> Cc: \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com>\n>> Cc: Gusted <gusted@codeberg.org>\n>\n> I think the Cc list got mixed up.\n\nI Cc'd you because of\nhttps://lore.kernel.org/git/f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com/\n\n\n-- \nCheers,\nToon\n"},{"id":"534296","messageId":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","threadId":"64531","inReplyTo":"20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com","subject":"[PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T21:47:07Z","receivedAt":"2026-01-20T21:47:22Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"We have had several reports[1][2] from users that have been thoroughly\nconfused by the default behaviour of git-last-modified(1). Most\nimportantly, when using the command on trees other than the root tree\nthe requests are seemingly ignoring that request and instead blame the\ntop-level subtree. For example:\n\n    $ git last-modified t/lib-bash.sh\n    acdfea4394db8e8b42f48e36f7726d64a909a89d t\n\nThis result is completely unexpected and feels buggy to anybody who is\nnot deeply familiar with git-last-modified(1). Furthermore, users\ntypically have a hard time to figure out the correct parameters.\n\nThis series changes the behavior to something that intuitively makes\nmore sense.\n\nThe change of the default behavior is done in the last commit, in the\nlead-up commits changes in the documentation are made.\n\n[1]: <f0c508cc-5c6b-4c4b-a3f3-0cdd8d1071e5@app.fastmail.com>\n[2]: <03f96860-29fc-42a7-a220-c3ec65eb8516@codeberg.org>\n\n---\nChanges in v3:\n- Rejoined the two commits about `-z and the two about `--max-depth`\n- In the end, drop `struct last_modified::recursive`. This ensures\n  the effect of `--recursive` or `--max-depth` depends on whatever comes\n  last.\n- Added a commit to rename `<path>` to `<pathspec>` in the docs.\n- Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com\n\nChanges in v2:\n- Change the default --max-depth.\n- Split up commits in smaller pieces.\n- Add more testss\n- Link to v1: https://patch.msgid.link/20251126-toon-last-modified-zzzz-v1-0-608350df0caa@iotcl.com\n\nCc: Gusted <gusted@codeberg.org>\n\n---\nToon Claes (4):\n      last-modified: clarify in the docs the command takes a pathspec\n      last-modified: document option '-z'\n      last-modified: document option '--max-depth'\n      last-modified: change default max-depth to 0\n\n Documentation/git-last-modified.adoc | 45 ++++++++++++++++++++++++++++--------\n builtin/last-modified.c              | 25 ++++++++++++--------\n t/t8020-last-modified.sh             | 35 ++++++++++++++++++++++++++++\n 3 files changed, 87 insertions(+), 18 deletions(-)\n\nRange-diff versus v2:\n\n-:  ---------- > 1:  1290dbb179 last-modified: clarify in the docs the command takes a pathspec\n1:  2c9fa32799 ! 2:  8ba5c59ae8 last-modified: document NUL termination\n    @@ Metadata\n     Author: Toon Claes <toon@iotcl.com>\n     \n      ## Commit message ##\n    -    last-modified: document NUL termination\n    +    last-modified: document option '-z'\n     \n         The command git-last-modified(1) already recognizes the option '-z', and\n         similar to many other commands this will make the output NUL-terminated\n         instead of using newlines. Although, this option is missing from the\n         documentation, so add it.\n     \n    +    In addition to that, to have '-z' also appear in the help output of `git\n    +    last-modified -h`, move the handling of '-z' to parse_options() in\n    +    builtin/last-modified.c itself.\n    +\n    +    Before, the parsing of option '-z' was done by diff_opt_parse(), which\n    +    is called by setup_revisions(). That would fill in `struct\n    +    diff_options::line_termination`, but that field was not used by the diff\n    +    machinery itself. Thus it makes more sense to have the handling of that\n    +    option completely in builtin/last-modified.c.\n    +\n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n      ## Documentation/git-last-modified.adoc ##\n    @@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show whe\n      SYNOPSIS\n      --------\n      [synopsis]\n    --git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n    +-git last-modified [--recursive] [--show-trees]\n     +git last-modified [--recursive] [--show-trees] [-z]\n    -+\t\t  [<revision-range>] [[--] <path>...]\n    + \t\t  [<revision-range>] [[--] <pathspec>...]\n      \n      DESCRIPTION\n    - -----------\n     @@ Documentation/git-last-modified.adoc: OPTIONS\n      \tShow tree entries even when recursing into them. It has no effect\n      \twithout `--recursive`.\n    @@ Documentation/git-last-modified.adoc: OPTIONS\n      \tOnly traverse commits in the specified revision range. When no\n      \t`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole\n     @@ Documentation/git-last-modified.adoc: OPTIONS\n    - \tWithout an optional path parameter, all files and subdirectories\n    - \tin path traversal the are included in the output.\n    + \tIf no _<pathspec>_ is given, all files and subdirectories are included.\n    + \tSee linkgit:gitglossary[7] for details on pathspec syntax.\n      \n     +OUTPUT\n     +------\n    @@ Documentation/git-last-modified.adoc: OPTIONS\n      linkgit:git-blame[1],\n     \n      ## builtin/last-modified.c ##\n    +@@ builtin/last-modified.c: struct last_modified {\n    + \tstruct rev_info rev;\n    + \tbool recursive;\n    + \tbool show_trees;\n    ++\tbool nul_termination;\n    + \n    + \tconst char **all_paths;\n    + \tsize_t all_paths_nr;\n    +@@ builtin/last-modified.c: static void last_modified_emit(struct last_modified *lm,\n    + \t\tputchar('^');\n    + \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n    + \n    +-\tif (lm->rev.diffopt.line_termination)\n    +-\t\twrite_name_quoted(path, stdout, '\\n');\n    +-\telse\n    ++\tif (lm->nul_termination)\n    + \t\tprintf(\"%s%c\", path, '\\0');\n    ++\telse\n    ++\t\twrite_name_quoted(path, stdout, '\\n');\n    + }\n    + \n    + static void mark_path(const char *path, const struct object_id *oid,\n     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n      \tstruct last_modified lm = { 0 };\n      \n      \tconst char * const last_modified_usage[] = {\n    --\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n    --\t\t   \"[<revision-range>] [[--] <path>...]\"),\n    +-\t\tN_(\"git last-modified [--recursive] [--show-trees]\\n\"\n     +\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n    -+\t\t   \"                  [<revision-range>] [[--] <path>...]\"),\n    + \t\t   \"                  [<revision-range>] [[--] <pathspec>...]\"),\n      \t\tNULL\n      \t};\n    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    + \t\t\t N_(\"recurse into subtrees\")),\n    + \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n    + \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n    ++\t\tOPT_BOOL('z', NULL, &lm.nul_termination,\n    ++\t\t\t N_(\"lines are separated with NUL character\")),\n    + \t\tOPT_END()\n    + \t};\n      \n2:  04589b4ccc < -:  ---------- last-modified: add option '-z' to help output\n3:  a45fe991ff < -:  ---------- last-modified: document option --max-depth\n4:  3103084474 ! 3:  a1f0178263 last-modified: add option '--max-depth' to help output\n    @@ Metadata\n     Author: Toon Claes <toon@iotcl.com>\n     \n      ## Commit message ##\n    -    last-modified: add option '--max-depth' to help output\n    +    last-modified: document option '--max-depth'\n    +\n    +    Option --max-depth is supported by git-last-modified(1), because it was\n    +    added to the diff machinery in a1dfa5448d (diff: teach tree-diff a\n    +    max-depth parameter, 2025-08-07).\n    +\n    +    This option is useful for everyday use of the git-last-modified(1)\n    +    command, so document it's existence in the man page.\n     \n    -    In previous commit option '--max-depth' was added to the documentation.\n         To have it also appear in the help output of `git last-modified -h`,\n         move the handling of '--max-depth' to parse_options() in\n    -    builtin/last-modified.c itself.\n    -\n    -    It enables us to change default behavior in a subsequent commit.\n    +    builtin/last-modified.c itself. This prepares for the change in default\n    +    behavior in the next commit.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    + ## Documentation/git-last-modified.adoc ##\n    +@@ Documentation/git-last-modified.adoc: git-last-modified - EXPERIMENTAL: Show when files were last modified\n    + SYNOPSIS\n    + --------\n    + [synopsis]\n    +-git last-modified [--recursive] [--show-trees] [-z]\n    ++git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n    + \t\t  [<revision-range>] [[--] <pathspec>...]\n    + \n    + DESCRIPTION\n    +@@ Documentation/git-last-modified.adoc: OPTIONS\n    + \tShow tree entries even when recursing into them. It has no effect\n    + \twithout `--recursive`.\n    + \n    ++`--max-depth=<depth>`::\n    ++\tFor each pathspec given on the command line, traverse at most `<depth>`\n    ++\tlevels into subtrees. A negative value means no limit.\n    ++\tThe default is 0, which shows all paths matching the pathspec\n    ++\twithout descending into subtrees.\n    ++\n    + `-z`::\n    + \tTerminate each line with a _NUL_ character rather than a newline.\n    + \n    +\n      ## builtin/last-modified.c ##\n     @@ builtin/last-modified.c: struct last_modified {\n      \tbool recursive;\n      \tbool show_trees;\n    - \tbool null_termination;\n    + \tbool nul_termination;\n     +\tint max_depth;\n      \n      \tconst char **all_paths;\n    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,\n      \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n      \tif (argc > 1) {\n      \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    + \tstruct last_modified lm = { 0 };\n    + \n    + \tconst char * const last_modified_usage[] = {\n    +-\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n    ++\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\\n\"\n    + \t\t   \"                  [<revision-range>] [[--] <pathspec>...]\"),\n    + \t\tNULL\n    + \t};\n     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n      \t\t\t N_(\"recurse into subtrees\")),\n      \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n      \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n     +\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n    -+\t\t\tN_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n    - \t\tOPT_BOOL('z', NULL, &lm.null_termination,\n    - \t\t\tN_(\"lines are separated with NUL character\")),\n    ++\t\t\t      N_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n    + \t\tOPT_BOOL('z', NULL, &lm.nul_termination,\n    + \t\t\t N_(\"lines are separated with NUL character\")),\n      \t\tOPT_END()\n      \t};\n      \n5:  3795e85ab5 ! 4:  81ce108196 last-modified: change default max-depth to 0\n    @@ Commit message\n     \n      ## Documentation/git-last-modified.adoc ##\n     @@ Documentation/git-last-modified.adoc: OPTIONS\n    + \n    + `-r`::\n      `--recursive`::\n    - \tInstead of showing tree entries, step into subtrees and show all entries\n    - \tinside them recursively.\n    -+\tThis is identical as setting `--max-depth=-1`.\n    +-\tInstead of showing tree entries, step into subtrees and show all entries\n    +-\tinside them recursively.\n    ++\tRecursively traverse into all subtrees. By default, the command only\n    ++\tshows tree entries matching the `<pathspec>`. With this option, it\n    ++\tdescends into subtrees and displays all entries within them.\n    ++\tEquivalent to `--max-depth=-1`.\n      \n      `-t`::\n      `--show-trees`::\n    -@@ Documentation/git-last-modified.adoc: OPTIONS\n    - `--max-depth=<depth>`::\n    - \tFor each pathspec given on the command line, descend at most `<depth>`\n    - \tlevels of directories. A negative value means no limit.\n    --\tSetting a positive value implies `--recursive`.\n    -+\tThe default depth is 0.\n    - \tCannot be combined with wildcards in the pathspec.\n    +-\tShow tree entries even when recursing into them. It has no effect\n    +-\twithout `--recursive`.\n    ++\tShow tree entries even when recursing into them.\n      \n    - `-z`::\n    + `--max-depth=<depth>`::\n    + \tFor each pathspec given on the command line, traverse at most `<depth>`\n     \n      ## builtin/last-modified.c ##\n    +@@ builtin/last-modified.c: define_commit_slab(active_paths_for_commit, struct bitmap *);\n    + struct last_modified {\n    + \tstruct hashmap paths;\n    + \tstruct rev_info rev;\n    +-\tbool recursive;\n    + \tbool show_trees;\n    + \tbool nul_termination;\n    + \tint max_depth;\n     @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm, struct repository *r,\n      \tlm->rev.no_commit_id = 1;\n      \tlm->rev.diff = 1;\n    @@ builtin/last-modified.c: static int last_modified_init(struct last_modified *lm,\n     -\t\tlm->rev.diffopt.max_depth_valid = 1;\n     -\t}\n     +\tlm->rev.diffopt.max_depth = lm->max_depth;\n    -+\tlm->rev.diffopt.max_depth_valid = !lm->recursive && lm->max_depth >= 0;\n    ++\tlm->rev.diffopt.max_depth_valid = lm->max_depth >= 0;\n      \n      \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n      \tif (argc > 1) {\n    +@@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n    + \t};\n    + \n    + \tstruct option last_modified_options[] = {\n    +-\t\tOPT_BOOL('r', \"recursive\", &lm.recursive,\n    +-\t\t\t N_(\"recurse into subtrees\")),\n    ++\t\tOPT_SET_INT('r', \"recursive\", &lm.max_depth,\n    ++\t\t\t    N_(\"recurse into subtrees\"), -1),\n    + \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n    + \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n    + \t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n     @@ builtin/last-modified.c: int cmd_last_modified(int argc, const char **argv, const char *prefix,\n      \t\tOPT_END()\n      \t};\n\n\n---\nbase-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\nchange-id: 20251114-toon-last-modified-zzzz-af9c1be74fc4\n\n"},{"id":"534297","messageId":"20260120-toon-last-modified-zzzz-v3-1-9bffd4968b0e@iotcl.com","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","subject":"[PATCH v3 1/4] last-modified: clarify in the docs the command takes a pathspec","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T21:47:08Z","receivedAt":"2026-01-20T21:47:32Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The documentation mentions git-last-modified(1) takes `<path>...`, but\nthat argument actually accepts a pathspec. Reword the documentation to\nreflect that.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 11 ++++++-----\n builtin/last-modified.c              |  4 ++--\n 2 files changed, 8 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 602843e095..7c3fd844b8 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,8 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]\n+git last-modified [--recursive] [--show-trees]\n+\t\t  [<revision-range>] [[--] <pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -39,10 +40,10 @@ OPTIONS\n \tspell `<revision-range>`, see the 'Specifying Ranges' section of\n \tlinkgit:gitrevisions[7].\n \n-`[--] <path>...`::\n-\tFor each _<path>_ given, the commit which last modified it is returned.\n-\tWithout an optional path parameter, all files and subdirectories\n-\tin path traversal the are included in the output.\n+`[--] <pathspec>...`::\n+\tShow the commit that last modified each path matching _<pathspec>_.\n+\tIf no _<pathspec>_ is given, all files and subdirectories are included.\n+\tSee linkgit:gitglossary[7] for details on pathspec syntax.\n \n SEE ALSO\n --------\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex c80f0535f6..e38e0bc4ca 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -510,8 +510,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = { 0 };\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] \"\n-\t\t   \"[<revision-range>] [[--] <path>...]\"),\n+\t\tN_(\"git last-modified [--recursive] [--show-trees]\\n\"\n+\t\t   \"                  [<revision-range>] [[--] <pathspec>...]\"),\n \t\tNULL\n \t};\n \n\n-- \n2.52.0\n\n"},{"id":"534298","messageId":"20260120-toon-last-modified-zzzz-v3-2-9bffd4968b0e@iotcl.com","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","subject":"[PATCH v3 2/4] last-modified: document option '-z'","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T21:47:09Z","receivedAt":"2026-01-20T21:47:41Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The command git-last-modified(1) already recognizes the option '-z', and\nsimilar to many other commands this will make the output NUL-terminated\ninstead of using newlines. Although, this option is missing from the\ndocumentation, so add it.\n\nIn addition to that, to have '-z' also appear in the help output of `git\nlast-modified -h`, move the handling of '-z' to parse_options() in\nbuiltin/last-modified.c itself.\n\nBefore, the parsing of option '-z' was done by diff_opt_parse(), which\nis called by setup_revisions(). That would fill in `struct\ndiff_options::line_termination`, but that field was not used by the diff\nmachinery itself. Thus it makes more sense to have the handling of that\noption completely in builtin/last-modified.c.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc | 21 ++++++++++++++++++++-\n builtin/last-modified.c              | 11 +++++++----\n 2 files changed, 27 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 7c3fd844b8..3760fd33a1 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees]\n+git last-modified [--recursive] [--show-trees] [-z]\n \t\t  [<revision-range>] [[--] <pathspec>...]\n \n DESCRIPTION\n@@ -33,6 +33,9 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`-z`::\n+\tTerminate each line with a _NUL_ character rather than a newline.\n+\n `<revision-range>`::\n \tOnly traverse commits in the specified revision range. When no\n \t`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole\n@@ -45,6 +48,22 @@ OPTIONS\n \tIf no _<pathspec>_ is given, all files and subdirectories are included.\n \tSee linkgit:gitglossary[7] for details on pathspec syntax.\n \n+OUTPUT\n+------\n+\n+The output is in the format:\n+\n+------------\n+ <oid> TAB <path> LF\n+------------\n+\n+If a path contains any special characters, the path is C-style quoted. To\n+avoid quoting, pass option `-z` to terminate each line with a NUL.\n+\n+------------\n+ <oid> TAB <path> NUL\n+------------\n+\n SEE ALSO\n --------\n linkgit:git-blame[1],\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex e38e0bc4ca..4060abfad3 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -55,6 +55,7 @@ struct last_modified {\n \tstruct rev_info rev;\n \tbool recursive;\n \tbool show_trees;\n+\tbool nul_termination;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -165,10 +166,10 @@ static void last_modified_emit(struct last_modified *lm,\n \t\tputchar('^');\n \tprintf(\"%s\\t\", oid_to_hex(&commit->object.oid));\n \n-\tif (lm->rev.diffopt.line_termination)\n-\t\twrite_name_quoted(path, stdout, '\\n');\n-\telse\n+\tif (lm->nul_termination)\n \t\tprintf(\"%s%c\", path, '\\0');\n+\telse\n+\t\twrite_name_quoted(path, stdout, '\\n');\n }\n \n static void mark_path(const char *path, const struct object_id *oid,\n@@ -510,7 +511,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = { 0 };\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees]\\n\"\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n \t\t   \"                  [<revision-range>] [[--] <pathspec>...]\"),\n \t\tNULL\n \t};\n@@ -520,6 +521,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_BOOL('z', NULL, &lm.nul_termination,\n+\t\t\t N_(\"lines are separated with NUL character\")),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.52.0\n\n"},{"id":"534299","messageId":"20260120-toon-last-modified-zzzz-v3-3-9bffd4968b0e@iotcl.com","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","subject":"[PATCH v3 3/4] last-modified: document option '--max-depth'","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T21:47:10Z","receivedAt":"2026-01-20T21:47:44Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --max-depth is supported by git-last-modified(1), because it was\nadded to the diff machinery in a1dfa5448d (diff: teach tree-diff a\nmax-depth parameter, 2025-08-07).\n\nThis option is useful for everyday use of the git-last-modified(1)\ncommand, so document it's existence in the man page.\n\nTo have it also appear in the help output of `git last-modified -h`,\nmove the handling of '--max-depth' to parse_options() in\nbuiltin/last-modified.c itself. This prepares for the change in default\nbehavior in the next commit.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc |  8 +++++++-\n builtin/last-modified.c              | 17 ++++++++++++++++-\n 2 files changed, 23 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 3760fd33a1..6f9b119bb6 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -9,7 +9,7 @@ git-last-modified - EXPERIMENTAL: Show when files were last modified\n SYNOPSIS\n --------\n [synopsis]\n-git last-modified [--recursive] [--show-trees] [-z]\n+git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\n \t\t  [<revision-range>] [[--] <pathspec>...]\n \n DESCRIPTION\n@@ -33,6 +33,12 @@ OPTIONS\n \tShow tree entries even when recursing into them. It has no effect\n \twithout `--recursive`.\n \n+`--max-depth=<depth>`::\n+\tFor each pathspec given on the command line, traverse at most `<depth>`\n+\tlevels into subtrees. A negative value means no limit.\n+\tThe default is 0, which shows all paths matching the pathspec\n+\twithout descending into subtrees.\n+\n `-z`::\n \tTerminate each line with a _NUL_ character rather than a newline.\n \ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 4060abfad3..28fc77a0f8 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -56,6 +56,7 @@ struct last_modified {\n \tbool recursive;\n \tbool show_trees;\n \tbool nul_termination;\n+\tint max_depth;\n \n \tconst char **all_paths;\n \tsize_t all_paths_nr;\n@@ -483,6 +484,12 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n \tlm->rev.diffopt.flags.recursive = lm->recursive;\n \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n \n+\tif (lm->max_depth >= 0) {\n+\t\tlm->rev.diffopt.flags.recursive = 1;\n+\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n+\t\tlm->rev.diffopt.max_depth_valid = 1;\n+\t}\n+\n \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n \tif (argc > 1) {\n \t\terror(_(\"unknown last-modified argument: %s\"), argv[1]);\n@@ -511,7 +518,7 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \tstruct last_modified lm = { 0 };\n \n \tconst char * const last_modified_usage[] = {\n-\t\tN_(\"git last-modified [--recursive] [--show-trees] [-z]\\n\"\n+\t\tN_(\"git last-modified [--recursive] [--show-trees] [--max-depth=<depth>] [-z]\\n\"\n \t\t   \"                  [<revision-range>] [[--] <pathspec>...]\"),\n \t\tNULL\n \t};\n@@ -521,11 +528,19 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"recurse into subtrees\")),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n+\t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n+\t\t\t      N_(\"maximum tree depth to recurse\"), PARSE_OPT_NONEG),\n \t\tOPT_BOOL('z', NULL, &lm.nul_termination,\n \t\t\t N_(\"lines are separated with NUL character\")),\n \t\tOPT_END()\n \t};\n \n+\t/*\n+\t * Set the default of a max-depth to \"unset\". This will change in a\n+\t * subsequent commit.\n+\t */\n+\tlm.max_depth = -1;\n+\n \targc = parse_options(argc, argv, prefix, last_modified_options,\n \t\t\t     last_modified_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\n\n-- \n2.52.0\n\n"},{"id":"534300","messageId":"20260120-toon-last-modified-zzzz-v3-4-9bffd4968b0e@iotcl.com","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","subject":"[PATCH v3 4/4] last-modified: change default max-depth to 0","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-01-20T21:47:11Z","receivedAt":"2026-01-20T21:47:51Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"By default git-last-modified(1) doesn't recurse into subtrees. So when\nthe pathspec contained a path in a subtree, the command would only print\nthe commit information about the parent tree of the path, like:\n\n    $ git last-modified -- path/file\n    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath\n\nChange the default behavior to give commit information about the exact\npath instead:\n\n    $ git last-modified -- path/file\n    aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath/file\n\nTo achieve this, the default max-depth is changed to 0 and recursive is\nalways enabled.\n\nThe handling of option '-r' is modified to disable a max-depth,\nresulting in the behavior of this option to remain unchanged.\n\nNo existing tests were modified, because there didn't exist any tests\ncovering the example above. But more tests are added to cover this now.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-last-modified.adoc |  9 +++++----\n builtin/last-modified.c              | 21 +++++----------------\n t/t8020-last-modified.sh             | 35 +++++++++++++++++++++++++++++++++++\n 3 files changed, 45 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/git-last-modified.adoc b/Documentation/git-last-modified.adoc\nindex 6f9b119bb6..d7d16fc4f7 100644\n--- a/Documentation/git-last-modified.adoc\n+++ b/Documentation/git-last-modified.adoc\n@@ -25,13 +25,14 @@ OPTIONS\n \n `-r`::\n `--recursive`::\n-\tInstead of showing tree entries, step into subtrees and show all entries\n-\tinside them recursively.\n+\tRecursively traverse into all subtrees. By default, the command only\n+\tshows tree entries matching the `<pathspec>`. With this option, it\n+\tdescends into subtrees and displays all entries within them.\n+\tEquivalent to `--max-depth=-1`.\n \n `-t`::\n `--show-trees`::\n-\tShow tree entries even when recursing into them. It has no effect\n-\twithout `--recursive`.\n+\tShow tree entries even when recursing into them.\n \n `--max-depth=<depth>`::\n \tFor each pathspec given on the command line, traverse at most `<depth>`\ndiff --git a/builtin/last-modified.c b/builtin/last-modified.c\nindex 28fc77a0f8..f7f4c5109c 100644\n--- a/builtin/last-modified.c\n+++ b/builtin/last-modified.c\n@@ -53,7 +53,6 @@ define_commit_slab(active_paths_for_commit, struct bitmap *);\n struct last_modified {\n \tstruct hashmap paths;\n \tstruct rev_info rev;\n-\tbool recursive;\n \tbool show_trees;\n \tbool nul_termination;\n \tint max_depth;\n@@ -481,14 +480,10 @@ static int last_modified_init(struct last_modified *lm, struct repository *r,\n \tlm->rev.no_commit_id = 1;\n \tlm->rev.diff = 1;\n \tlm->rev.diffopt.flags.no_recursive_diff_tree_combined = 1;\n-\tlm->rev.diffopt.flags.recursive = lm->recursive;\n+\tlm->rev.diffopt.flags.recursive = 1;\n \tlm->rev.diffopt.flags.tree_in_recursive = lm->show_trees;\n-\n-\tif (lm->max_depth >= 0) {\n-\t\tlm->rev.diffopt.flags.recursive = 1;\n-\t\tlm->rev.diffopt.max_depth = lm->max_depth;\n-\t\tlm->rev.diffopt.max_depth_valid = 1;\n-\t}\n+\tlm->rev.diffopt.max_depth = lm->max_depth;\n+\tlm->rev.diffopt.max_depth_valid = lm->max_depth >= 0;\n \n \targc = setup_revisions(argc, argv, &lm->rev, NULL);\n \tif (argc > 1) {\n@@ -524,8 +519,8 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t};\n \n \tstruct option last_modified_options[] = {\n-\t\tOPT_BOOL('r', \"recursive\", &lm.recursive,\n-\t\t\t N_(\"recurse into subtrees\")),\n+\t\tOPT_SET_INT('r', \"recursive\", &lm.max_depth,\n+\t\t\t    N_(\"recurse into subtrees\"), -1),\n \t\tOPT_BOOL('t', \"show-trees\", &lm.show_trees,\n \t\t\t N_(\"show tree entries when recursing into subtrees\")),\n \t\tOPT_INTEGER_F(0, \"max-depth\", &lm.max_depth,\n@@ -535,12 +530,6 @@ int cmd_last_modified(int argc, const char **argv, const char *prefix,\n \t\tOPT_END()\n \t};\n \n-\t/*\n-\t * Set the default of a max-depth to \"unset\". This will change in a\n-\t * subsequent commit.\n-\t */\n-\tlm.max_depth = -1;\n-\n \targc = parse_options(argc, argv, prefix, last_modified_options,\n \t\t\t     last_modified_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT |\ndiff --git a/t/t8020-last-modified.sh b/t/t8020-last-modified.sh\nindex 50f4312f71..3944d2e153 100755\n--- a/t/t8020-last-modified.sh\n+++ b/t/t8020-last-modified.sh\n@@ -93,6 +93,41 @@ test_expect_success 'last-modified subdir recursive' '\n \tEOF\n '\n \n+test_expect_success 'last-modified subdir non-recursive' '\n+\tcheck_last_modified a <<-\\EOF\n+\t3 a\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified path in subdir non-recursive' '\n+\tcheck_last_modified a/file <<-\\EOF\n+\t2 a/file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified subdir with wildcard non-recursive' '\n+\tcheck_last_modified a/* <<-\\EOF\n+\t3 a/b\n+\t2 a/file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified with negative max-depth' '\n+\tcheck_last_modified --max-depth=-1 <<-\\EOF\n+\t3 a/b/file\n+\t2 a/file\n+\t1 file\n+\tEOF\n+'\n+\n+test_expect_success 'last-modified with max-depth of 1' '\n+\tcheck_last_modified --max-depth=1 <<-\\EOF\n+\t3 a/b\n+\t2 a/file\n+\t1 file\n+\tEOF\n+'\n+\n test_expect_success 'last-modified from non-HEAD commit' '\n \tcheck_last_modified HEAD^ <<-\\EOF\n \t2 a\n\n-- \n2.52.0\n\n"},{"id":"534371","messageId":"xmqqpl72u7vc.fsf@gitster.g","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-0-9bffd4968b0e@iotcl.com","subject":"Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-21T18:40:39Z","receivedAt":"2026-01-21T18:40:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> Changes in v3:\n> - Rejoined the two commits about `-z and the two about `--max-depth`\n> - In the end, drop `struct last_modified::recursive`. This ensures\n>   the effect of `--recursive` or `--max-depth` depends on whatever comes\n>   last.\n> - Added a commit to rename `<path>` to `<pathspec>` in the docs.\n> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com\n\nThis round looked reasonable to me.  Shall we mark it for 'next'?\n\nThanks.\n"},{"id":"534608","messageId":"0b97e8fd-1dfc-4a38-9c77-22467f38fd3b@app.fastmail.com","threadId":"64531","inReplyTo":"20260120-toon-last-modified-zzzz-v3-4-9bffd4968b0e@iotcl.com","subject":"Re: [PATCH v3 4/4] last-modified: change default max-depth to 0","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-25T11:24:54Z","receivedAt":"2026-01-25T11:25:15Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jan 20, 2026, at 22:47, Toon Claes wrote:\n> By default git-last-modified(1) doesn't recurse into subtrees. So when\n> the pathspec contained a path in a subtree, the command would only print\n> the commit information about the parent tree of the path, like:\n>\n>     $ git last-modified -- path/file\n>     aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath\n>\n> Change the default behavior to give commit information about the exact\n> path instead:\n>\n>     $ git last-modified -- path/file\n>     aaa0aab1bbb2bcc3ccc4ddd5dde6eee7eff8fff9\tpath/file\n>\n> To achieve this, the default max-depth is changed to 0 and recursive is\n> always enabled.\n>\n> The handling of option '-r' is modified to disable a max-depth,\n> resulting in the behavior of this option to remain unchanged.\n>\n> No existing tests were modified, because there didn't exist any tests\n> covering the example above. But more tests are added to cover this now.\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n\nThanks. I think this makes more sense.\n\n>[snip]\n> --- a/Documentation/git-last-modified.adoc\n> +++ b/Documentation/git-last-modified.adoc\n> @@ -25,13 +25,14 @@ OPTIONS\n>\n>  `-r`::\n>  `--recursive`::\n> -\tInstead of showing tree entries, step into subtrees and show all entries\n> -\tinside them recursively.\n> +\tRecursively traverse into all subtrees. By default, the command only\n> +\tshows tree entries matching the `<pathspec>`. With this option, it\n\nThis should be s/`<pathspec>`/_<pathspec>_ since this is a `[synopsis]`\ndoc. But this is in `next` so it will have to be left for another time.\n\n>[snip]\n"},{"id":"535045","messageId":"CAOLa=ZQEU7kD6DEj446Wj19XUQP_1bOXYK1xsg5rbjnw7W3sDw@mail.gmail.com","threadId":"64531","inReplyTo":"xmqqpl72u7vc.fsf@gitster.g","subject":"Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-02-03T09:58:10Z","receivedAt":"2026-02-03T09:58:12Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Toon Claes <toon@iotcl.com> writes:\n>\n>> Changes in v3:\n>> - Rejoined the two commits about `-z and the two about `--max-depth`\n>> - In the end, drop `struct last_modified::recursive`. This ensures\n>>   the effect of `--recursive` or `--max-depth` depends on whatever comes\n>>   last.\n>> - Added a commit to rename `<path>` to `<pathspec>` in the docs.\n>> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com\n>\n> This round looked reasonable to me.  Shall we mark it for 'next'?\n>\n> Thanks.\n\nI also did a round of review and think it is indeed ready for 'next'.\n\nThanks\n"},{"id":"535080","messageId":"xmqq7bstu3k0.fsf@gitster.g","threadId":"64531","inReplyTo":"CAOLa=ZQEU7kD6DEj446Wj19XUQP_1bOXYK1xsg5rbjnw7W3sDw@mail.gmail.com","subject":"Re: [PATCH v3 0/4] Change git-last-modified(1) default behavior and add documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-03T17:42:23Z","receivedAt":"2026-02-03T17:42:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> Toon Claes <toon@iotcl.com> writes:\n>>\n>>> Changes in v3:\n>>> - Rejoined the two commits about `-z and the two about `--max-depth`\n>>> - In the end, drop `struct last_modified::recursive`. This ensures\n>>>   the effect of `--recursive` or `--max-depth` depends on whatever comes\n>>>   last.\n>>> - Added a commit to rename `<path>` to `<pathspec>` in the docs.\n>>> - Link to v2: https://patch.msgid.link/20260116-toon-last-modified-zzzz-v2-0-79e44f2806fe@iotcl.com\n>>\n>> This round looked reasonable to me.  Shall we mark it for 'next'?\n>>\n>> Thanks.\n>\n> I also did a round of review and think it is indeed ready for 'next'.\n\n;-)\n"}]}