{"thread":{"id":"66022","subject":"[PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP","startedAt":"2026-07-16T16:55:43Z","lastAt":"2026-08-12T20:30:02Z","messageCount":48,"participants":["Christian Couder","Junio C Hamano","D. Ben Knoble","Elijah Newren"],"isPatch":true,"patchVersion":1,"patchTotal":7},"messages":[{"id":"548431","messageId":"20260716165517.433849-1-christian.couder@gmail.com","threadId":"66022","inReplyTo":null,"subject":"[PATCH 0/7] fast-import: standardize usage string and SYNOPSIS","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:10Z","receivedAt":"2026-07-16T16:55:42Z","isPatch":true,"body":"The goal of this series is to improve on `git fast-import`'s usage\nstring as it is obsolete in many ways.\n\nAlong the way it modernizes \"builtin/fast-import.c\" mostly by using\n`struct option`, by starting to remove global variables and libify\nthat command, and by introducing a new `OPT_HIDDEN_GROUP` macro.\n\n`struct option` is used only for the usage string for now and there\nare still many global variables left, so it's left to future work to\nfinish on these directions.\n\nBut this is already enough to standardize the usage string and make it\nconsistent with the SYNOPSIS in the docs, so that the command can be\nremoved from \"t/t0450/adoc-help-mismatches\".\n\nOverview of the patches\n=======================\n\n  - Patch 1/7: Introduces OPT_HIDDEN_GROUP and improves on the hidden\n    option tests.\n\n  - Patches 2/7 and 3/7: Improves on the parse-options API docs.\n\n  - Patch 4/7: Cleans up an 'i' loop counter variable in\n    cmd_fast_import().\n\n  - Patches 5/7 and 6/7: Starts libifying \"builtin/fast-import.c\" by\n    introducing a 'struct fast_import_state' and using it to store\n    some global variables.\n\n  - Patch 7/7: Improves the usage string and SYNOPSIS by introducing\n    `struct option`.\n\nCI tests\n========\n\nThey all pass, see: https://github.com/chriscool/git/actions/runs/29513714019\n\nChristian Couder (7):\n  parse-options: introduce OPT_HIDDEN_GROUP\n  api-parse-options.adoc: document per-option flags\n  api-parse-options.adoc: document hidden and OPT_*_F option macros\n  fast-import: localize 'i' into the 'for' loops using it\n  fast-import: introduce 'struct fast_import_state'\n  fast-import: move command state globals into 'struct\n    fast_import_state'\n  fast-import: use struct option for usage string\n\n Documentation/git-fast-import.adoc            |   2 +-\n .../technical/api-parse-options.adoc          |  79 ++++\n builtin/fast-import.c                         | 372 +++++++++++-------\n parse-options.c                               |   4 +-\n parse-options.h                               |   5 +\n t/helper/test-parse-options.c                 |   4 +\n t/t0040-parse-options.sh                      |  25 +-\n t/t0450/adoc-help-mismatches                  |   1 -\n 8 files changed, 344 insertions(+), 148 deletions(-)\n\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548430","messageId":"20260716165517.433849-2-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:11Z","receivedAt":"2026-07-16T16:55:43Z","isPatch":true,"body":"Hidden options are not shown by `git <cmd> -h`, but are still shown by\n`git <cmd> --help-all`. If there are a lot of hidden options or if they\ndon't belong to the same categories as other options, there is\ncurrently no way to properly group them.\n\nUsing `OPT_GROUP(\"Foo\")` means that \"Foo\" will always be shown which we\ndon't want if that group contains only hidden options.\n\nTo provide a way to have groups shown only when hidden options are\nshown, let's implement an OPT_HIDDEN_GROUP macro.\n\nTo test this new macro, let's also improve `test-tool parse-options`\nand test its output with `--help-all`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n parse-options.c               |  4 ++--\n parse-options.h               |  5 +++++\n t/helper/test-parse-options.c |  4 ++++\n t/t0040-parse-options.sh      | 25 ++++++++++++++++++++++++-\n 4 files changed, 35 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex f4647e0099..640e600de8 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1404,6 +1404,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \n \t\tif (opts->type == OPTION_SUBCOMMAND)\n \t\t\tcontinue;\n+\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n+\t\t\tcontinue;\n \t\tif (opts->type == OPTION_GROUP) {\n \t\t\tfputc('\\n', outfile);\n \t\t\tneed_newline = 0;\n@@ -1411,8 +1413,6 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t\t\t\tfprintf(outfile, \"%s\\n\", _(opts->help));\n \t\t\tcontinue;\n \t\t}\n-\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n-\t\t\tcontinue;\n \n \t\tif (need_newline) {\n \t\t\tfputc('\\n', outfile);\ndiff --git a/parse-options.h b/parse-options.h\nindex 0d1f738f8d..a28b3cd942 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -236,6 +236,11 @@ struct option {\n \t.type = OPTION_GROUP, \\\n \t.help = (h), \\\n }\n+#define OPT_HIDDEN_GROUP(h) { \\\n+\t.type = OPTION_GROUP, \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_HIDDEN, \\\n+}\n #define OPT_BIT(s, l, v, h, b)      OPT_BIT_F(s, l, v, h, b, 0)\n #define OPT_BITOP(s, l, v, h, set, clear) { \\\n \t.type = OPTION_BITOP, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 68579d83f3..f181f0c02d 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -209,6 +209,10 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Alias\"),\n \t\tOPT_STRING('A', \"alias-source\", &string, \"string\", \"get a string\"),\n \t\tOPT_ALIAS('Z', \"alias-target\", \"alias-source\"),\n+\t\tOPT_HIDDEN_GROUP(\"Hidden options\"),\n+\t\tOPT_HIDDEN_BOOL(0, \"hidden-bool\", &boolean, \"get a boolean\"),\n+\t\tOPT_INTEGER_F('k', \"hidden-integer\", &integer, \"get a integer\",\n+\t\t\t      PARSE_OPT_HIDDEN),\n \t\tOPT_END(),\n \t};\n \tint ret = 0;\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..4040333185 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -7,7 +7,7 @@ test_description='our own option parser'\n \n . ./test-lib.sh\n \n-cat >expect <<\\EOF\n+cat >expect-part1 <<\\EOF\n usage: test-tool parse-options <options>\n \n     A helper function for the parse-options API.\n@@ -41,6 +41,9 @@ String options\n     --[no-]string2 <str>  get another string\n     --[no-]st <st>        get another string (pervert ordering)\n     -o <str>              get another string\n+EOF\n+\n+cat >expect-part2 <<\\EOF\n     --longhelp            help text of this entry\n                           spans multiple lines\n     --[no-]list <str>     add str to list\n@@ -67,12 +70,32 @@ Alias\n \n EOF\n \n+cat >expect-noop <<\\EOF\n+    --[no-]obsolete       no-op (backward compatibility)\n+EOF\n+\n+cat >expect-hidden <<\\EOF\n+Hidden options\n+    --[no-]hidden-bool    get a boolean\n+    -k, --[no-]hidden-integer <n>\n+                          get a integer\n+\n+EOF\n+\n test_expect_success 'test help' '\n+\tcat expect-part1 expect-part2 >expect &&\n \ttest_must_fail test-tool parse-options -h >output 2>output.err &&\n \ttest_must_be_empty output.err &&\n \ttest_cmp expect output\n '\n \n+test_expect_success 'test --help-all shows hidden group and options' '\n+\tcat expect-part1 expect-noop expect-part2 expect-hidden >expect-help-all &&\n+\ttest_must_fail test-tool parse-options --help-all >output 2>output.err &&\n+\ttest_must_be_empty output.err &&\n+\ttest_cmp expect-help-all output\n+'\n+\n mv expect expect.err\n \n check () {\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548432","messageId":"20260716165517.433849-3-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 2/7] api-parse-options.adoc: document per-option flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:12Z","receivedAt":"2026-07-16T16:55:48Z","isPatch":true,"body":"The \"Flags\" section in \"Documentation/technical/api-parse-options.adoc\"\ndocuments the flags that can be passed to parse_options() itself. It\ndoes not, however, document the flags that can be set on individual\noptions through the `flags` member of `struct option` (and through the\n`OPT_*_F()` macro variants).\n\nThese per-option flags are used throughout the codebase (for example\n`PARSE_OPT_HIDDEN` is used to hide an option from `-h` while still\nshowing it with `--help-all`), but a reader currently has to dig into\n\"parse-options.h\" to find them.\n\nTo remediate that, let's add an \"Option flags\" subsection to the\n\"Data Structure\" section, just before the list of option macros.\n\nLet's also make it explicit that these are distinct from the\nparse_options() flags described earlier, and let's describe the `-h`\nversus `--help-all` behavior for `PARSE_OPT_HIDDEN`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n .../technical/api-parse-options.adoc          | 61 +++++++++++++++++++\n 1 file changed, 61 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex 880eb94642..fb4580e755 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -150,6 +150,67 @@ Data Structure\n \n The main data structure is an array of the `option` struct,\n say `static struct option builtin_add_options[]`.\n+\n+Option flags\n+~~~~~~~~~~~~\n+\n+Each option can carry flags in the `flags` field of its `option`\n+struct. These are per-option flags and are distinct from the\n+`parse_options()` flags described above; they are usually set through\n+the `OPT_*_F()` macro variants (see below) rather than by hand. They\n+are the bitwise-or of:\n+\n+`PARSE_OPT_OPTARG`::\n+\tThe option's argument is optional, i.e. both `--option` and\n+\t`--option=<value>` are accepted.\n+\n+`PARSE_OPT_NOARG`::\n+\tThe option takes no argument at all. Using `--option=<value>`\n+\tis rejected.\n+\n+`PARSE_OPT_NONEG`::\n+\tDisable the automatically generated negated `--no-option`\n+\tform.\n+\n+`PARSE_OPT_HIDDEN`::\n+\tHide the option: it is omitted from the usage shown by\n+\t`git <cmd> -h`, but is still shown by `git <cmd> --help-all`.\n+\tThe option is parsed as usual either way. This is meant for\n+\tdeprecated, advanced or otherwise uncommon options.\n+\n+`PARSE_OPT_LASTARG_DEFAULT`::\n+\tUse the default value (`defval`) when the option is used\n+\twithout an argument, even for an option that normally requires\n+\tone. Only the last argument on the command line takes effect.\n+\n+`PARSE_OPT_NODASH`::\n+\tThe option is a single character without a leading dash, such\n+\tas the `+` used by some commands.\n+\n+`PARSE_OPT_LITERAL_ARGHELP`::\n+\tUse the argument help string (`argh`) verbatim in the usage\n+\toutput instead of surrounding it with `<>` or `[]`. Useful when\n+\t`argh` already contains a hand-formatted description.\n+\n+`PARSE_OPT_FROM_ALIAS`::\n+\tInternal flag, set on options that were expanded from a\n+\tconfigured alias. It should not be set by callers.\n+\n+`PARSE_OPT_NOCOMPLETE`::\n+\tDo not offer this option for completion.\n+\n+`PARSE_OPT_COMP_ARG`::\n+\tThe option's argument, rather than the option itself, is what\n+\tshould be completed.\n+\n+`PARSE_OPT_CMDMODE`::\n+\tThe option is one of several mutually exclusive \"command mode\"\n+\toptions that share the same variable. Using more than one of\n+\tthem at once is rejected.\n+\n+Macros\n+~~~~~~\n+\n There are some macros to easily define options:\n \n `OPT__ABBREV(&int_var)`::\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548433","messageId":"20260716165517.433849-4-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 3/7] api-parse-options.adoc: document hidden and OPT_*_F option macros","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:13Z","receivedAt":"2026-07-16T16:55:54Z","isPatch":true,"body":"In \"Documentation/technical/api-parse-options.adoc\", the list of option\nmacros does not mention the `OPT_*_F()` macro variants that take a\ntrailing `flags` argument, nor the `OPT_HIDDEN_GROUP()` and\n`OPT_HIDDEN_BOOL()` convenience macros.\n\nNow that a previous commit documents the per-option flags, let's\ndocument these macros too:\n\n  - Add a paragraph explaining the `OPT_*_F` convention and how it\n    relates to the per-option flags.\n\n  - Document `OPT_HIDDEN_GROUP()`, introduced in a previous commit,\n    right after `OPT_GROUP()`.\n\n  - Document `OPT_HIDDEN_BOOL()` right after `OPT_BOOL()`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/technical/api-parse-options.adoc | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex fb4580e755..0e10327d07 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -213,6 +213,13 @@ Macros\n \n There are some macros to easily define options:\n \n+Many of the macros below have an `_F` variant (for example `OPT_BOOL_F`,\n+`OPT_STRING_F`, `OPT_INTEGER_F`, `OPT_SET_INT_F`, `OPT_BIT_F` and\n+`OPT_CALLBACK_F`) that takes an additional trailing `flags` argument.\n+That argument is the bitwise-or of the per-option flags described in the\n+\"Option flags\" section above; the non-`_F` macros are simply defined\n+with `flags` set to `0`.\n+\n `OPT__ABBREV(&int_var)`::\n \tAdd `--abbrev[=<n>]`.\n \n@@ -236,10 +243,21 @@ There are some macros to easily define options:\n \tdescribes the group or an empty string.\n \tStart the description with an upper-case letter.\n \n+`OPT_HIDDEN_GROUP(description)`::\n+\tLike `OPT_GROUP()`, but the group header carries\n+\t`PARSE_OPT_HIDDEN`, so it is only shown by `--help-all` and not\n+\tby `-h`. Use it to label a group that contains only hidden\n+\toptions, which would otherwise show an empty header under `-h`.\n+\n `OPT_BOOL(short, long, &int_var, description)`::\n \tIntroduce a boolean option. `int_var` is set to one with\n \t`--option` and set to zero with `--no-option`.\n \n+`OPT_HIDDEN_BOOL(short, long, &int_var, description)`::\n+\tLike `OPT_BOOL()`, but the option carries `PARSE_OPT_HIDDEN`,\n+\tso it is hidden from `-h` while still being shown by\n+\t`--help-all`.\n+\n `OPT_COUNTUP(short, long, &int_var, description)`::\n \tIntroduce a count-up option.\n \tEach use of `--option` increments `int_var`, starting from zero\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548434","messageId":"20260716165517.433849-5-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 4/7] fast-import: localize 'i' into the 'for' loops using it","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:14Z","receivedAt":"2026-07-16T16:55:54Z","isPatch":true,"body":"In cmd_fast_import(), a\tlocal variable 'i' is defined as an\n`unsigned int` and then used as a loop counter in four different\n`for (i = ...; i < ...; i++) { ... }` loops.\n\nBut in three out of the four cases, `unsigned int` isn't the best type\nto use.\n\nTo give each loop counter the type matching its bound\n(int/unsigned/size_t), let's localize 'i' into each loop that uses it.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fast-import.c | 10 ++++------\n 1 file changed, 4 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex aa656c5195..fd4e13b7ca 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3936,8 +3936,6 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n-\tunsigned int i;\n-\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3958,7 +3956,7 @@ int cmd_fast_import(int argc,\n \t * line to override stream data). But we must do an early parse of any\n \t * command-line options that impact how we interpret the feature lines.\n \t */\n-\tfor (i = 1; i < argc; i++) {\n+\tfor (int i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n@@ -3971,7 +3969,7 @@ int cmd_fast_import(int argc,\n \tglobal_prefix = prefix;\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n-\tfor (i = 0; i < (cmd_save - 1); i++)\n+\tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n \trc_free[cmd_save - 1].next = NULL;\n \n@@ -4034,9 +4032,9 @@ int cmd_fast_import(int argc,\n \n \tif (show_stats) {\n \t\tuintmax_t total_count = 0, duplicate_count = 0;\n-\t\tfor (i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n \t\t\ttotal_count += object_count_by_type[i];\n-\t\tfor (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n \t\t\tduplicate_count += duplicate_count_by_type[i];\n \n \t\tfprintf(stderr, \"%s statistics:\\n\", argv[0]);\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548435","messageId":"20260716165517.433849-6-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 5/7] fast-import: introduce 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:15Z","receivedAt":"2026-07-16T16:55:56Z","isPatch":true,"body":"\"builtin/fast-import.c\" uses a large number of global variables. This\nmakes it harder than necessary to reason about and improve. Especially\nadding new features requires adding more global variables, while\nmodernizing and eventually libifying the code becomes more and more\ndifficult.\n\nTo start reverting the sad trend to more and more globals and to start\ncleaning things up, let's introduce a 'struct fast_import_state' and\npass an instance of it as the first argument to many functions.\n\nThis is similar to what was done for \"builtin/apply.c\" by introducing a\n'struct apply_state', see 07d7e290ff (apply: move 'struct apply_state'\nto a header file, 2016-08-11) and related commits.\n\nAs a first step only the 'global_argc', 'global_argv' and\n'global_prefix' variables are moved into the new struct. More variables\nwill be moved into it in the following commits.\n\nSome functions receive the new 'state' parameter only to pass it\nalong or for future use, so they are marked with UNUSED for now to\nsatisfy '-Werror=unused-parameter'.\n\nThis is a mostly mechanical refactoring with no intended behavior\nchange.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fast-import.c | 262 ++++++++++++++++++++++--------------------\n 1 file changed, 138 insertions(+), 124 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex fd4e13b7ca..d20353679a 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -184,10 +184,6 @@ static int failure;\n static FILE *pack_edges;\n static unsigned int show_stats = 1;\n static unsigned int quiet;\n-static int global_argc;\n-static const char **global_argv;\n-static const char *global_prefix;\n-\n static enum sign_mode signed_tag_mode = SIGN_VERBATIM;\n static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n static const char *signed_commit_keyid;\n@@ -276,10 +272,27 @@ static kh_oid_map_t *sub_oid_map;\n /* Where to write output of cat-blob commands */\n static int cat_blob_fd = STDOUT_FILENO;\n \n-static void parse_argv(void);\n-static void parse_get_mark(const char *p);\n-static void parse_cat_blob(const char *p);\n-static void parse_ls(const char *p, struct branch *b);\n+/* Command state */\n+struct fast_import_state {\n+\tint argc;\n+\tconst char **argv;\n+\tconst char *prefix;\n+};\n+\n+static void fast_import_state_init(struct fast_import_state *state,\n+\t\t\t\t   int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n+{\n+\tmemset(state, 0, sizeof(*state));\n+\tstate->argc = argc;\n+\tstate->argv = argv;\n+\tstate->prefix = prefix;\n+}\n+\n+static void parse_argv(struct fast_import_state *state);\n+static void parse_get_mark(struct fast_import_state *state, const char *p);\n+static void parse_cat_blob(struct fast_import_state *state, const char *p);\n+static void parse_ls(struct fast_import_state *state, const char *p, struct branch *b);\n \n static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p)\n {\n@@ -1844,7 +1857,7 @@ static void read_marks(void)\n }\n \n \n-static int read_next_command(void)\n+static int read_next_command(struct fast_import_state *state)\n {\n \tstatic int stdin_eof = 0;\n \n@@ -1866,7 +1879,7 @@ static int read_next_command(void)\n \t\t\tif (!seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n-\t\t\t\tparse_argv();\n+\t\t\t\tparse_argv(state);\n \t\t\t}\n \n \t\t\trc = rc_free;\n@@ -1898,22 +1911,22 @@ static void skip_optional_lf(void)\n \t\tungetc(term_char, stdin);\n }\n \n-static void parse_mark(void)\n+static void parse_mark(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"mark :\", &v)) {\n \t\tnext_mark = strtoumax(v, NULL, 10);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \telse\n \t\tnext_mark = 0;\n }\n \n-static void parse_original_identifier(void)\n+static void parse_original_identifier(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"original-oid \", &v))\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n }\n \n static int parse_data(struct strbuf *sb, uintmax_t limit, uintmax_t *len_res)\n@@ -2067,11 +2080,11 @@ static void parse_and_store_blob(\n \t}\n }\n \n-static void parse_new_blob(void)\n+static void parse_new_blob(struct fast_import_state *state)\n {\n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tparse_and_store_blob(&last_blob, NULL, next_mark);\n }\n \n@@ -2367,7 +2380,7 @@ static void parse_path_space(struct strbuf *sb, const char *p,\n \t(*endp)++;\n }\n \n-static void file_change_m(const char *p, struct branch *b)\n+static void file_change_m(struct fast_import_state *state, const char *p, struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct object_entry *oe;\n@@ -2434,10 +2447,10 @@ static void file_change_m(const char *p, struct branch *b)\n \t\tif (S_ISDIR(mode))\n \t\t\tdie(_(\"directories cannot be specified 'inline': %s\"),\n \t\t\t\tcommand_buf.buf);\n-\t\twhile (read_next_command() != EOF) {\n+\t\twhile (read_next_command(state) != EOF) {\n \t\t\tconst char *v;\n \t\t\tif (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\t\tparse_cat_blob(v);\n+\t\t\t\tparse_cat_blob(state, v);\n \t\t\telse {\n \t\t\t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t\t\t\tbreak;\n@@ -2511,7 +2524,7 @@ static void file_change_cr(const char *p, struct branch *b, int rename)\n \t\tleaf.tree);\n }\n \n-static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n+static void note_change_n(struct fast_import_state *state, const char *p, struct branch *b, unsigned char *old_fanout)\n {\n \tstruct object_entry *oe;\n \tstruct branch *s;\n@@ -2576,7 +2589,7 @@ static void note_change_n(const char *p, struct branch *b, unsigned char *old_fa\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), p);\n \n \tif (inline_data) {\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t} else if (oe) {\n \t\tif (oe->type != OBJ_BLOB)\n@@ -2643,7 +2656,7 @@ static void parse_from_existing(struct branch *b)\n \t}\n }\n \n-static int parse_objectish(struct branch *b, const char *objectish)\n+static int parse_objectish(struct fast_import_state *state, struct branch *b, const char *objectish)\n {\n \tstruct branch *s;\n \tstruct object_id oid;\n@@ -2686,31 +2699,31 @@ static int parse_objectish(struct branch *b, const char *objectish)\n \t\tb->branch_tree.tree = NULL;\n \t}\n \n-\tread_next_command();\n+\tread_next_command(state);\n \treturn 1;\n }\n \n-static int parse_from(struct branch *b)\n+static int parse_from(struct fast_import_state *state, struct branch *b)\n {\n \tconst char *from;\n \n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, from);\n+\treturn parse_objectish(state, b, from);\n }\n \n-static int parse_objectish_with_prefix(struct branch *b, const char *prefix)\n+static int parse_objectish_with_prefix(struct fast_import_state *state, struct branch *b, const char *prefix)\n {\n \tconst char *base;\n \n \tif (!skip_prefix(command_buf.buf, prefix, &base))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, base);\n+\treturn parse_objectish(state, b, base);\n }\n \n-static struct hash_list *parse_merge(unsigned int *count)\n+static struct hash_list *parse_merge(struct fast_import_state *state, unsigned int *count)\n {\n \tstruct hash_list *list = NULL, **tail = &list, *n;\n \tconst char *from;\n@@ -2744,7 +2757,7 @@ static struct hash_list *parse_merge(unsigned int *count)\n \t\ttail = &n->next;\n \n \t\t(*count)++;\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \treturn list;\n }\n@@ -2755,7 +2768,7 @@ struct signature_data {\n \tstruct strbuf data;   /* The actual signature data */\n };\n \n-static void parse_one_signature(struct signature_data *sig, const char *v)\n+static void parse_one_signature(struct fast_import_state *state, struct signature_data *sig, const char *v)\n {\n \tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n \tchar *space = strchr(args, ' ');\n@@ -2780,15 +2793,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v)\n \t\twarning(_(\"'unknown' signature format in gpgsig\"));\n \n \t/* Read signature data */\n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&sig->data, 0, NULL);\n }\n \n-static void discard_one_signature(void)\n+static void discard_one_signature(struct fast_import_state *state)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&data, 0, NULL);\n \tstrbuf_release(&data);\n }\n@@ -2826,13 +2839,14 @@ static void store_signature(struct signature_data *stored_sig,\n \t}\n }\n \n-static void import_one_signature(struct signature_data *sig_sha1,\n+static void import_one_signature(struct fast_import_state *state,\n+\t\t\t\t struct signature_data *sig_sha1,\n \t\t\t\t struct signature_data *sig_sha256,\n \t\t\t\t const char *v)\n {\n \tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n \n-\tparse_one_signature(&sig, v);\n+\tparse_one_signature(state, &sig, v);\n \n \tif (!strcmp(sig.hash_algo, \"sha1\"))\n \t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n@@ -2946,7 +2960,7 @@ static void handle_signature_if_invalid(struct strbuf *new_data,\n \tstrbuf_release(&tmp_buf);\n }\n \n-static void parse_new_commit(const char *arg)\n+static void parse_new_commit(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n@@ -2964,16 +2978,16 @@ static void parse_new_commit(const char *arg)\n \tif (!b)\n \t\tb = new_branch(arg);\n \n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tif (skip_prefix(command_buf.buf, \"author \", &v)) {\n \t\tauthor = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (skip_prefix(command_buf.buf, \"committer \", &v)) {\n \t\tcommitter = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (!committer)\n \t\tdie(_(\"expected committer but didn't get one\"));\n@@ -2989,7 +3003,7 @@ static void parse_new_commit(const char *arg)\n \t\t\twarning(_(\"stripping a commit signature\"));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n-\t\t\tdiscard_one_signature();\n+\t\t\tdiscard_one_signature(state);\n \t\t\tbreak;\n \n \t\t/* Second, modes that parse the signature */\n@@ -3000,24 +3014,24 @@ static void parse_new_commit(const char *arg)\n \t\tcase SIGN_STRIP_IF_INVALID:\n \t\tcase SIGN_SIGN_IF_INVALID:\n \t\tcase SIGN_ABORT_IF_INVALID:\n-\t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n+\t\t\timport_one_signature(state, &sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n \t\t/* Third, BUG */\n \t\tdefault:\n \t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n \t\t}\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tparse_data(&msg, 0, NULL);\n-\tread_next_command();\n-\tparse_from(b);\n-\tmerge_list = parse_merge(&merge_count);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n+\tmerge_list = parse_merge(state, &merge_count);\n \n \t/* ensure the branch is active/loaded */\n \tif (!b->branch_tree.tree || !max_active_branches) {\n@@ -3030,7 +3044,7 @@ static void parse_new_commit(const char *arg)\n \t/* file_change* */\n \twhile (command_buf.len > 0) {\n \t\tif (skip_prefix(command_buf.buf, \"M \", &v))\n-\t\t\tfile_change_m(v, b);\n+\t\t\tfile_change_m(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"D \", &v))\n \t\t\tfile_change_d(v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"R \", &v))\n@@ -3038,18 +3052,18 @@ static void parse_new_commit(const char *arg)\n \t\telse if (skip_prefix(command_buf.buf, \"C \", &v))\n \t\t\tfile_change_cr(v, b, 0);\n \t\telse if (skip_prefix(command_buf.buf, \"N \", &v))\n-\t\t\tnote_change_n(v, b, &prev_fanout);\n+\t\t\tnote_change_n(state, v, b, &prev_fanout);\n \t\telse if (!strcmp(\"deleteall\", command_buf.buf))\n \t\t\tfile_change_deleteall(b);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, b);\n+\t\t\tparse_ls(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(state, v);\n \t\telse {\n \t\t\tunread_command_buf = 1;\n \t\t\tbreak;\n \t\t}\n-\t\tif (read_next_command() == EOF)\n+\t\tif (read_next_command(state) == EOF)\n \t\t\tbreak;\n \t}\n \n@@ -3187,7 +3201,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \t}\n }\n \n-static void parse_new_tag(const char *arg)\n+static void parse_new_tag(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tconst char *from;\n@@ -3206,8 +3220,8 @@ static void parse_new_tag(const char *arg)\n \telse\n \t\tfirst_tag = t;\n \tlast_tag = t;\n-\tread_next_command();\n-\tparse_mark();\n+\tread_next_command(state);\n+\tparse_mark(state);\n \n \t/* from ... */\n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n@@ -3235,15 +3249,15 @@ static void parse_new_tag(const char *arg)\n \t\t\ttype = oe->type;\n \t} else\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), from);\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* original-oid ... */\n-\tparse_original_identifier();\n+\tparse_original_identifier(state);\n \n \t/* tagger ... */\n \tif (skip_prefix(command_buf.buf, \"tagger \", &v)) {\n \t\ttagger = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t} else\n \t\ttagger = NULL;\n \n@@ -3274,7 +3288,7 @@ static void parse_new_tag(const char *arg)\n \t\tt->pack_id = pack_id;\n }\n \n-static void parse_reset_branch(const char *arg)\n+static void parse_reset_branch(struct fast_import_state *state, const char *arg)\n {\n \tstruct branch *b;\n \tconst char *tag_name;\n@@ -3291,8 +3305,8 @@ static void parse_reset_branch(const char *arg)\n \t}\n \telse\n \t\tb = new_branch(arg);\n-\tread_next_command();\n-\tparse_from(b);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n \tif (b->delete && skip_prefix(b->name, \"refs/tags/\", &tag_name)) {\n \t\t/*\n \t\t * Elsewhere, we call dump_branches() before dump_tags(),\n@@ -3377,7 +3391,7 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid)\n \t\tfree(buf);\n }\n \n-static void parse_get_mark(const char *p)\n+static void parse_get_mark(struct fast_import_state *state UNUSED, const char *p)\n {\n \tstruct object_entry *oe;\n \tchar output[GIT_MAX_HEXSZ + 2];\n@@ -3394,7 +3408,7 @@ static void parse_get_mark(const char *p)\n \tcat_blob_write(output, the_hash_algo->hexsz + 1);\n }\n \n-static void parse_cat_blob(const char *p)\n+static void parse_cat_blob(struct fast_import_state *state UNUSED, const char *p)\n {\n \tstruct object_entry *oe;\n \tstruct object_id oid;\n@@ -3559,7 +3573,7 @@ static void print_ls(int mode, const unsigned char *hash, const char *path)\n \tcat_blob_write(line.buf, line.len);\n }\n \n-static void parse_ls(const char *p, struct branch *b)\n+static void parse_ls(struct fast_import_state *state UNUSED, const char *p, struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct tree_entry *root = NULL;\n@@ -3606,13 +3620,13 @@ static void checkpoint(void)\n \tdump_marks();\n }\n \n-static void parse_checkpoint(void)\n+static void parse_checkpoint(struct fast_import_state *state UNUSED)\n {\n \tcheckpoint_requested = 1;\n \tskip_optional_lf();\n }\n \n-static void parse_progress(void)\n+static void parse_progress(struct fast_import_state *state UNUSED)\n {\n \tfwrite(command_buf.buf, 1, command_buf.len, stdout);\n \tfputc('\\n', stdout);\n@@ -3620,36 +3634,36 @@ static void parse_progress(void)\n \tskip_optional_lf();\n }\n \n-static void parse_alias(void)\n+static void parse_alias(struct fast_import_state *state)\n {\n \tstruct object_entry *e;\n \tstruct branch b;\n \n \tskip_optional_lf();\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* mark ... */\n-\tparse_mark();\n+\tparse_mark(state);\n \tif (!next_mark)\n \t\tdie(_(\"expected 'mark' command, got %s\"), command_buf.buf);\n \n \t/* to ... */\n \tmemset(&b, 0, sizeof(b));\n-\tif (!parse_objectish_with_prefix(&b, \"to \"))\n+\tif (!parse_objectish_with_prefix(state, &b, \"to \"))\n \t\tdie(_(\"expected 'to' command, got %s\"), command_buf.buf);\n \te = find_object(&b.oid);\n \tassert(e);\n \tinsert_mark(&marks, next_mark, e);\n }\n \n-static char* make_fast_import_path(const char *path)\n+static char* make_fast_import_path(struct fast_import_state *state, const char *path)\n {\n \tif (!relative_marks_paths || is_absolute_path(path))\n-\t\treturn prefix_filename(global_prefix, path);\n+\t\treturn prefix_filename(state->prefix, path);\n \treturn repo_git_path(the_repository, \"info/fast-import/%s\", path);\n }\n \n-static void option_import_marks(const char *marks,\n+static void option_import_marks(struct fast_import_state *state, const char *marks,\n \t\t\t\t\tint from_stream, int ignore_missing)\n {\n \tif (import_marks_file) {\n@@ -3662,7 +3676,7 @@ static void option_import_marks(const char *marks,\n \t}\n \n \tfree(import_marks_file);\n-\timport_marks_file = make_fast_import_path(marks);\n+\timport_marks_file = make_fast_import_path(state, marks);\n \timport_marks_file_from_stream = from_stream;\n \timport_marks_file_ignore_missing = ignore_missing;\n }\n@@ -3702,13 +3716,13 @@ static void option_active_branches(const char *branches)\n \tmax_active_branches = ulong_arg(\"--active-branches\", branches);\n }\n \n-static void option_export_marks(const char *marks)\n+static void option_export_marks(struct fast_import_state *state, const char *marks)\n {\n \tfree(export_marks_file);\n-\texport_marks_file = make_fast_import_path(marks);\n+\texport_marks_file = make_fast_import_path(state, marks);\n }\n \n-static void option_cat_blob_fd(const char *fd)\n+static void option_cat_blob_fd(struct fast_import_state *state UNUSED, const char *fd)\n {\n \tunsigned long n = ulong_arg(\"--cat-blob-fd\", fd);\n \tif (n > (unsigned long) INT_MAX)\n@@ -3716,16 +3730,16 @@ static void option_cat_blob_fd(const char *fd)\n \tcat_blob_fd = (int) n;\n }\n \n-static void option_export_pack_edges(const char *edges)\n+static void option_export_pack_edges(struct fast_import_state *state, const char *edges)\n {\n-\tchar *fn = prefix_filename(global_prefix, edges);\n+\tchar *fn = prefix_filename(state->prefix, edges);\n \tif (pack_edges)\n \t\tfclose(pack_edges);\n \tpack_edges = xfopen(fn, \"a\");\n \tfree(fn);\n }\n \n-static void option_rewrite_submodules(const char *arg, struct string_list *list)\n+static void option_rewrite_submodules(struct fast_import_state *state, const char *arg, struct string_list *list)\n {\n \tstruct mark_set *ms;\n \tFILE *fp;\n@@ -3737,7 +3751,7 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tf++;\n \tCALLOC_ARRAY(ms, 1);\n \n-\tf = prefix_filename(global_prefix, f);\n+\tf = prefix_filename(state->prefix, f);\n \tfp = fopen(f, \"r\");\n \tif (!fp)\n \t\tdie_errno(_(\"cannot read '%s'\"), f);\n@@ -3750,7 +3764,7 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tfree(s);\n }\n \n-static int parse_one_option(const char *option)\n+static int parse_one_option(struct fast_import_state *state, const char *option)\n {\n \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n \t\tunsigned long v;\n@@ -3774,7 +3788,7 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n \t\toption_active_branches(option);\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n-\t\toption_export_pack_edges(option);\n+\t\toption_export_pack_edges(state, option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n \t\tif (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))\n \t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n@@ -3795,34 +3809,34 @@ static int parse_one_option(const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n {\n \tif (from_stream && !allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n \n-static int parse_one_feature(const char *feature, int from_stream)\n+static int parse_one_feature(struct fast_import_state *state, const char *feature, int from_stream)\n {\n \tconst char *arg;\n \n \tif (skip_prefix(feature, \"date-format=\", &arg)) {\n \t\toption_date_format(arg);\n \t} else if (skip_prefix(feature, \"import-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 0);\n+\t\tcheck_unsafe_feature(state, \"import-marks\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 0);\n \t} else if (skip_prefix(feature, \"import-marks-if-exists=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks-if-exists\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 1);\n+\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 1);\n \t} else if (skip_prefix(feature, \"export-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(feature, from_stream);\n-\t\toption_export_marks(arg);\n+\t\tcheck_unsafe_feature(state, feature, from_stream);\n+\t\toption_export_marks(state, arg);\n \t} else if (!strcmp(feature, \"alias\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (skip_prefix(feature, \"rewrite-submodules-to=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_to);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_to);\n \t} else if (skip_prefix(feature, \"rewrite-submodules-from=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_from);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_from);\n \t} else if (!strcmp(feature, \"get-mark\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (!strcmp(feature, \"cat-blob\")) {\n@@ -3844,23 +3858,23 @@ static int parse_one_feature(const char *feature, int from_stream)\n \treturn 1;\n }\n \n-static void parse_feature(const char *feature)\n+static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n-\tif (parse_one_feature(feature, 1))\n+\tif (parse_one_feature(state, feature, 1))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support feature %s.\"), feature);\n }\n \n-static void parse_option(const char *option)\n+static void parse_option(struct fast_import_state *state, const char *option)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n-\tif (parse_one_option(option))\n+\tif (parse_one_option(state, option))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support option: %s\"), option);\n@@ -3896,12 +3910,12 @@ static void git_pack_config(void)\n static const char fast_import_usage[] =\n \"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n \n-static void parse_argv(void)\n+static void parse_argv(struct fast_import_state *state)\n {\n \tunsigned int i;\n \n-\tfor (i = 1; i < global_argc; i++) {\n-\t\tconst char *a = global_argv[i];\n+\tfor (i = 1; i < state->argc; i++) {\n+\t\tconst char *a = state->argv[i];\n \n \t\tif (*a != '-' || !strcmp(a, \"--\"))\n \t\t\tbreak;\n@@ -3909,20 +3923,20 @@ static void parse_argv(void)\n \t\tif (!skip_prefix(a, \"--\", &a))\n \t\t\tdie(_(\"unknown option %s\"), a);\n \n-\t\tif (parse_one_option(a))\n+\t\tif (parse_one_option(state, a))\n \t\t\tcontinue;\n \n-\t\tif (parse_one_feature(a, 0))\n+\t\tif (parse_one_feature(state, a, 0))\n \t\t\tcontinue;\n \n \t\tif (skip_prefix(a, \"cat-blob-fd=\", &a)) {\n-\t\t\toption_cat_blob_fd(a);\n+\t\t\toption_cat_blob_fd(state, a);\n \t\t\tcontinue;\n \t\t}\n \n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n-\tif (i != global_argc)\n+\tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n \tseen_data_command = 1;\n@@ -3936,6 +3950,8 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n+\tstruct fast_import_state state;\n+\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3964,9 +3980,7 @@ int cmd_fast_import(int argc,\n \t\t\tallow_unsafe_features = 1;\n \t}\n \n-\tglobal_argc = argc;\n-\tglobal_argv = argv;\n-\tglobal_prefix = prefix;\n+\tfast_import_state_init(&state, argc, argv, prefix);\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n@@ -3976,34 +3990,34 @@ int cmd_fast_import(int argc,\n \tstart_packfile();\n \tset_die_routine(die_nicely);\n \tset_checkpoint_signal();\n-\twhile (read_next_command() != EOF) {\n+\twhile (read_next_command(&state) != EOF) {\n \t\tconst char *v;\n \t\tif (!strcmp(\"blob\", command_buf.buf))\n-\t\t\tparse_new_blob();\n+\t\t\tparse_new_blob(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"commit \", &v))\n-\t\t\tparse_new_commit(v);\n+\t\t\tparse_new_commit(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"tag \", &v))\n-\t\t\tparse_new_tag(v);\n+\t\t\tparse_new_tag(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"reset \", &v))\n-\t\t\tparse_reset_branch(v);\n+\t\t\tparse_reset_branch(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, NULL);\n+\t\t\tparse_ls(&state, v, NULL);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"get-mark \", &v))\n-\t\t\tparse_get_mark(v);\n+\t\t\tparse_get_mark(&state, v);\n \t\telse if (!strcmp(\"checkpoint\", command_buf.buf))\n-\t\t\tparse_checkpoint();\n+\t\t\tparse_checkpoint(&state);\n \t\telse if (!strcmp(\"done\", command_buf.buf))\n \t\t\tbreak;\n \t\telse if (!strcmp(\"alias\", command_buf.buf))\n-\t\t\tparse_alias();\n+\t\t\tparse_alias(&state);\n \t\telse if (starts_with(command_buf.buf, \"progress \"))\n-\t\t\tparse_progress();\n+\t\t\tparse_progress(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"feature \", &v))\n-\t\t\tparse_feature(v);\n+\t\t\tparse_feature(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"option git \", &v))\n-\t\t\tparse_option(v);\n+\t\t\tparse_option(&state, v);\n \t\telse if (starts_with(command_buf.buf, \"option \"))\n \t\t\t/* ignore non-git options*/;\n \t\telse\n@@ -4015,7 +4029,7 @@ int cmd_fast_import(int argc,\n \n \t/* argv hasn't been parsed yet, do so */\n \tif (!seen_data_command)\n-\t\tparse_argv();\n+\t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n \t\tdie(_(\"stream ends early\"));\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548436","messageId":"20260716165517.433849-7-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 6/7] fast-import: move command state globals into 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:16Z","receivedAt":"2026-07-16T16:55:58Z","isPatch":true,"body":"A previous commit introduced 'struct fast_import_state' to hold some\ncommand state, and reduce the need for global variables.\n\nLet's continue in the same direction and move two more global variables\nthat describe the command state into it: 'seen_data_command' and\n'allow_unsafe_features'.\n\nAll the sites accessing these variables are already in functions that\nreceive the 'state' parameter (or in cmd_fast_import() which owns the\nstruct), so no additional threading is needed.\n\nAs 'state->allow_unsafe_features' is now dereferenced in\ncheck_unsafe_feature(), its 'state' parameter is no longer unused, so\nthe UNUSED marker is removed.\n\nThe fast_import_state_init() call is moved up before the early\ncommand-line scan for '--allow-unsafe-features', so that this option\ncan be recorded directly into the struct without being clobbered by\nthe memset() in fast_import_state_init().\n\nThis is a mechanical refactoring with no intended behavior change.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fast-import.c | 24 ++++++++++++------------\n 1 file changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex d20353679a..53f5d39173 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -257,9 +257,7 @@ static struct recent_command *rc_free;\n static unsigned int cmd_save = 100;\n static uintmax_t next_mark;\n static struct strbuf new_data = STRBUF_INIT;\n-static int seen_data_command;\n static int require_explicit_termination;\n-static int allow_unsafe_features;\n \n /* Signal handling */\n static volatile sig_atomic_t checkpoint_requested;\n@@ -277,6 +275,8 @@ struct fast_import_state {\n \tint argc;\n \tconst char **argv;\n \tconst char *prefix;\n+\tint seen_data_command;\n+\tint allow_unsafe_features;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n@@ -1876,7 +1876,7 @@ static int read_next_command(struct fast_import_state *state)\n \t\t\tif (stdin_eof)\n \t\t\t\treturn EOF;\n \n-\t\t\tif (!seen_data_command\n+\t\t\tif (!state->seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n \t\t\t\tparse_argv(state);\n@@ -3809,9 +3809,9 @@ static int parse_one_option(struct fast_import_state *state, const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state, const char *feature, int from_stream)\n {\n-\tif (from_stream && !allow_unsafe_features)\n+\tif (from_stream && !state->allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n@@ -3860,7 +3860,7 @@ static int parse_one_feature(struct fast_import_state *state, const char *featur\n \n static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n \tif (parse_one_feature(state, feature, 1))\n@@ -3871,7 +3871,7 @@ static void parse_feature(struct fast_import_state *state, const char *feature)\n \n static void parse_option(struct fast_import_state *state, const char *option)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n \tif (parse_one_option(state, option))\n@@ -3939,7 +3939,7 @@ static void parse_argv(struct fast_import_state *state)\n \tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n-\tseen_data_command = 1;\n+\tstate->seen_data_command = 1;\n \tif (import_marks_file)\n \t\tread_marks();\n \tbuild_mark_map(&sub_marks_from, &sub_marks_to);\n@@ -3954,6 +3954,8 @@ int cmd_fast_import(int argc,\n \n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n+\tfast_import_state_init(&state, argc, argv, prefix);\n+\n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\n \n@@ -3977,11 +3979,9 @@ int cmd_fast_import(int argc,\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n \t\tif (!strcmp(arg, \"--allow-unsafe-features\"))\n-\t\t\tallow_unsafe_features = 1;\n+\t\t\tstate.allow_unsafe_features = 1;\n \t}\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n-\n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n@@ -4028,7 +4028,7 @@ int cmd_fast_import(int argc,\n \t}\n \n \t/* argv hasn't been parsed yet, do so */\n-\tif (!seen_data_command)\n+\tif (!state.seen_data_command)\n \t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548437","messageId":"20260716165517.433849-8-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH 7/7] fast-import: use struct option for usage string","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-16T16:55:17Z","receivedAt":"2026-07-16T16:56:02Z","isPatch":true,"body":"Currently `git fast-import -h` shows the following on a single line:\n\n  usage : git fast-import [--date-format=<f>] [--max-pack-size=<n>] \\\n                          [--big-file-threshold=<n>] [--depth=<n>] \\\n                          [--active-branches=<n>] \\\n                          [--export-marks=<marks.file>]\n\nThis output has a number of issues like:\n\n  - It's missing a lot of options.\n  - It's not consistent with the SYNOPSIS section of the doc.\n  - With `--help-all` instead of `-h` additional hidden options should\n    be shown, but that's not the case.\n  - It's not standard style anymore.\n  - Most other Git commands show additional lines for most of the\n    options they support.\n\nAlso while most commands use the parse-options API to handle their\noptions, \"builtin/fast-import.c\" still doesn't use it.\n\nLet's improve on that by using the parse-options API to display the\noptions when `-h` and `--help-all` are used.\n\nWhile at it, let's make the SYNOPSIS section of\n\"Documentation/git-fast-import.adoc\" consistent with the new usage\nstring.\n\nThis deliberately leaves it to future work to also use the\nparse-options API to actually parse the options.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/git-fast-import.adoc |  2 +-\n builtin/fast-import.c              | 86 +++++++++++++++++++++++++++---\n t/t0450/adoc-help-mismatches       |  1 -\n 3 files changed, 81 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex d68bc52b7e..7c5900e048 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -9,7 +9,7 @@ git-fast-import - Backend for fast Git data importers\n SYNOPSIS\n --------\n [verse]\n-frontend | 'git fast-import' [<options>]\n+'git fast-import' [<options>]\n \n DESCRIPTION\n -----------\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 53f5d39173..a2952b273f 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -30,6 +30,7 @@\n #include \"khash.h\"\n #include \"date.h\"\n #include \"gpg-interface.h\"\n+#include \"parse-options.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -277,16 +278,18 @@ struct fast_import_state {\n \tconst char *prefix;\n \tint seen_data_command;\n \tint allow_unsafe_features;\n+\tstruct option *option;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n \t\t\t\t   int argc, const char **argv,\n-\t\t\t\t   const char *prefix)\n+\t\t\t\t   const char *prefix, struct option *option)\n {\n \tmemset(state, 0, sizeof(*state));\n \tstate->argc = argc;\n \tstate->argv = argv;\n \tstate->prefix = prefix;\n+\tstate->option = option;\n }\n \n static void parse_argv(struct fast_import_state *state);\n@@ -3907,8 +3910,10 @@ static void git_pack_config(void)\n \trepo_config(the_repository, git_default_config, NULL);\n }\n \n-static const char fast_import_usage[] =\n-\"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n+static const char *const fast_import_usage[] = {\n+\tN_(\"git fast-import [<options>]\"),\n+\tNULL\n+};\n \n static void parse_argv(struct fast_import_state *state)\n {\n@@ -3937,7 +3942,7 @@ static void parse_argv(struct fast_import_state *state)\n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n \tif (i != state->argc)\n-\t\tusage(fast_import_usage);\n+\t\tusage_with_options(fast_import_usage, state->option);\n \n \tstate->seen_data_command = 1;\n \tif (import_marks_file)\n@@ -3952,9 +3957,78 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\tshow_usage_if_asked(argc, argv, fast_import_usage);\n+\tunsigned long pack_size_limit, big_file_threshold, depth, active_branches;\n+\tchar *edges, *signed_commits, *signed_tags, *date_format, *import_marks;\n+\tchar *import_marks_if_exists, *export_marks, *submodules_from, *submodules_to;\n+\tint opt_quiet, opt_show_stats, opt_relative_marks, opt_force, opt_done;\n+\tint opt_allow_unsafe;\n+\tint cat_blob;\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n+\t/*\n+\t * NEEDSWORK: For now this is used only to render\n+\t * `-h`/`--help-all` usage messages. The actual parsing is\n+\t * done by parse_one_option()/parse_one_feature().\n+\t */\n+\tstruct option fast_import_options[] = {\n+\t\tOPT_GROUP(N_(\"Common\")),\n+\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n+\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"stats\", &opt_show_stats,\n+\t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n+\t\t\t   PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"quiet\", &opt_quiet,\n+\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"force\", &opt_force,\n+\t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"done\", &opt_done,\n+\t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n+\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n+\t\t\t     N_(\"maximum size of each output pack file\")),\n+\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n+\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n+\t\tOPT_UNSIGNED(0, \"depth\", &depth,\n+\t\t\t     N_(\"maximum delta depth\")),\n+\t\tOPT_UNSIGNED(0, \"active-branches\", &active_branches,\n+\t\t\t     N_(\"maximum number of branches to maintain active\")),\n+\t\tOPT_GROUP(N_(\"Marks\")),\n+\t\tOPT_STRING_F(0, \"import-marks\", &import_marks, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-marks\", &export_marks, N_(\"file\"),\n+\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL(0, \"relative-marks\", &opt_relative_marks,\n+\t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n+\t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-to\", &submodules_to, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_GROUP(N_(\"Signing\")),\n+\t\tOPT_STRING_F(0, \"signed-commits\", &signed_commits, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed commits\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"signed-tags\", &signed_tags, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed tags\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n+\t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &opt_allow_unsafe,\n+\t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n+\t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n+\t\t\t     N_(\"dump edge commits to <file>\"),\n+\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob,\n+\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n+\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_END()\n+\t};\n+\n+\tshow_usage_with_options_if_asked(argc, argv, fast_import_usage, fast_import_options);\n+\n+\tfast_import_state_init(&state, argc, argv, prefix, fast_import_options);\n \n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\ndiff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches\nindex e8d6c13ccd..85b039a4be 100644\n--- a/t/t0450/adoc-help-mismatches\n+++ b/t/t0450/adoc-help-mismatches\n@@ -13,7 +13,6 @@ credential\n credential-cache\n credential-store\n fast-export\n-fast-import\n fetch-pack\n fmt-merge-msg\n format-patch\n-- \n2.55.0.185.g9120d2b5c0\n\n"},{"id":"548459","messageId":"xmqqcxwmeiwq.fsf@gitster.g","threadId":"66022","inReplyTo":"20260716165517.433849-2-christian.couder@gmail.com","subject":"Re: [PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-16T21:14:45Z","receivedAt":"2026-07-16T21:14:47Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Hidden options are not shown by `git <cmd> -h`, but are still shown by\n> `git <cmd> --help-all`. If there are a lot of hidden options or if they\n> don't belong to the same categories as other options, there is\n> currently no way to properly group them.\n>\n> Using `OPT_GROUP(\"Foo\")` means that \"Foo\" will always be shown which we\n> don't want if that group contains only hidden options.\n>\n> To provide a way to have groups shown only when hidden options are\n> shown, let's implement an OPT_HIDDEN_GROUP macro.\n>\n> To test this new macro, let's also improve `test-tool parse-options`\n> and test its output with `--help-all`.\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n\nWe have been doing this for quite some time, but I just noticed\nthat the 'From' address your MUA uses (\"Christian Couder\n<christian.couder@gmail.com>\") does not match your Sign-off.  Could\nyou add an in-body 'From:' line if you plan to keep sending your\npatches from the Gmail address?\n\nI suppose nobody has noticed it so far because .mailmap hides the\ndiscrepancy once the commit lands.\n\nThe changes in this step looks alright, though.\n\nThanks.\n"},{"id":"548461","messageId":"xmqq4ihyehyb.fsf@gitster.g","threadId":"66022","inReplyTo":"20260716165517.433849-8-christian.couder@gmail.com","subject":"Re: [PATCH 7/7] fast-import: use struct option for usage string","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-16T21:35:24Z","receivedAt":"2026-07-16T21:35:27Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> +\tunsigned long pack_size_limit, big_file_threshold, depth, active_branches;\n> +\tchar *edges, *signed_commits, *signed_tags, *date_format, *import_marks;\n> +\tchar *import_marks_if_exists, *export_marks, *submodules_from, *submodules_to;\n> +\tint opt_quiet, opt_show_stats, opt_relative_marks, opt_force, opt_done;\n> +\tint opt_allow_unsafe;\n> +\tint cat_blob;\n>  \n> -\tfast_import_state_init(&state, argc, argv, prefix);\n> +\t/*\n> +\t * NEEDSWORK: For now this is used only to render\n> +\t * `-h`/`--help-all` usage messages. The actual parsing is\n> +\t * done by parse_one_option()/parse_one_feature().\n> +\t */\n\nOK, I am a bit torn on this.  On one hand:\n\n (1) I do agree that it would be nice to eventually have\n     fast_import_state_init() (or some other helper that groks\n     argc/argv) use this options array to parse the command line\n     arguments.\n\n (2) I am sympathetic to the position that doing so is a bit\n     outside the scope of this series, whose focus is strictly on\n     \"git fast-import -h\" and nothing else.\n\n (3) I suspect that when fast_import_state_init() does start using\n     the options array to initialize the state, the parsed results\n     will not be stored in the variables this caller currently holds,\n     but will instead live inside the fast_import_state structure.\n\nSo in that sense, the huge list of unused function-local variables\nabove are merely throw-away placeholders.  When the real code is\nwritten, they will disappear, and the references to them in the\nfast_import_options[] array will have to be updated to point to\nmembers of the structure (or global variables).\n\nStill, seeing all of those variables left uninitialized leaves a\nslightly sour taste.  And because of (3), it would be a clear waste\nof time to go through the motions of initializing these throw-away\nlocals.\n\nPerhaps we would end up in a better position if we bent (2) a bit.\nAfter all, my hesitation likely stems from the feeling that this\nseries stops short at a slightly awkward spot, having already\ncompleted 90% of the journey.\n\nFor example, instead of inventing a local, throw-away\n\"pack_size_limit\" variable, wouldn't it make more sense to refer to\nthe existing global \"max_packsize\" variable from the options[]\narray below?\n\n> +\tstruct option fast_import_options[] = {\n> +\t\tOPT_GROUP(N_(\"Common\")),\n> +\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n> +\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n> +\t\tOPT_BOOL_F(0, \"stats\", &opt_show_stats,\n> +\t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n> +\t\t\t   PARSE_OPT_NONEG),\n> +\t\tOPT_BOOL_F(0, \"quiet\", &opt_quiet,\n> +\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n> +\t\tOPT_BOOL_F(0, \"force\", &opt_force,\n> +\t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n> +\t\tOPT_BOOL_F(0, \"done\", &opt_done,\n> +\t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n> +\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n> +\t\t\t     N_(\"maximum size of each output pack file\")),\n> +\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n\nThanks.\n"},{"id":"548463","messageId":"CALnO6CCauJhCmgKjV6QJ44cZD=2ah6=rJaV0+2ZMYTo7ycghEA@mail.gmail.com","threadId":"66022","inReplyTo":"xmqqcxwmeiwq.fsf@gitster.g","subject":".mailmap etiquette (was \"Re: [PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP\")","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-07-16T22:14:56Z","receivedAt":"2026-07-16T22:15:08Z","isPatch":true,"body":"On Thu, Jul 16, 2026 at 5:17 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > Hidden options are not shown by `git <cmd> -h`, but are still shown by\n> > `git <cmd> --help-all`. If there are a lot of hidden options or if they\n> > don't belong to the same categories as other options, there is\n> > currently no way to properly group them.\n> >\n> > Using `OPT_GROUP(\"Foo\")` means that \"Foo\" will always be shown which we\n> > don't want if that group contains only hidden options.\n> >\n> > To provide a way to have groups shown only when hidden options are\n> > shown, let's implement an OPT_HIDDEN_GROUP macro.\n> >\n> > To test this new macro, let's also improve `test-tool parse-options`\n> > and test its output with `--help-all`.\n> >\n> > Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> > ---\n>\n> We have been doing this for quite some time, but I just noticed\n> that the 'From' address your MUA uses (\"Christian Couder\n> <christian.couder@gmail.com>\") does not match your Sign-off.  Could\n> you add an in-body 'From:' line if you plan to keep sending your\n> patches from the Gmail address?\n>\n> I suppose nobody has noticed it so far because .mailmap hides the\n> discrepancy once the commit lands.\n>\n> The changes in this step looks alright, though.\n>\n> Thanks.\n\nSpeaking of .mailmap… if I were going to send future patches under a\nnew email address, would you prefer\n\n(a) a series with the 1st commit being a .mailmap update (subsequent\ncommits bearing the new email address, of course, but unrelated to the\n.mailmap update)\n(b) a one-patch email with a .mailmap update\n(c) the same as (b), but only after commits with the new email address\nhave stabilized in next or master\n\n?\n\n[PS happy to drop anyone from CC who doesn't want this one; list\netiquette is a bit unclear to me on this kind of subject change,\nthough I would lean towards emptying CC and keeping only Junio…]\n\n-- \nD. Ben Knoble\n"},{"id":"548464","messageId":"xmqqqzl2d0s7.fsf@gitster.g","threadId":"66022","inReplyTo":"CALnO6CCauJhCmgKjV6QJ44cZD=2ah6=rJaV0+2ZMYTo7ycghEA@mail.gmail.com","subject":"Re: .mailmap etiquette (was \"Re: [PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP\")","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-16T22:31:36Z","receivedAt":"2026-07-16T22:31:39Z","isPatch":true,"body":"\"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n\n> Speaking of .mailmap… if I were going to send future patches under a\n> new email address, would you prefer\n>\n> (a) a series with the 1st commit being a .mailmap update (subsequent\n> commits bearing the new email address, of course, but unrelated to the\n> .mailmap update)\n> (b) a one-patch email with a .mailmap update\n> (c) the same as (b), but only after commits with the new email address\n> have stabilized in next or master\n\nI am not sure what sort of complexity you are anticipating, but\nhaving you send (b) and me applying it directly to 'master'\nwould be the simplest approach, wouldn't it?  After all, adding\na new entry to .mailmap does not invalidate your old identity;\nit merely links the new one to the same person.\n\n\n\n\n"},{"id":"549506","messageId":"CAP8UFD3hnJLy=pE-K0yy+FisPdLQxAk6457Kur--gv+k4bB6ig@mail.gmail.com","threadId":"66022","inReplyTo":"xmqqcxwmeiwq.fsf@gitster.g","subject":"Re: [PATCH 1/7] parse-options: introduce OPT_HIDDEN_GROUP","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-03T17:19:00Z","receivedAt":"2026-08-03T17:19:13Z","isPatch":true,"body":"On Thu, Jul 16, 2026 at 11:14 PM Junio C Hamano <gitster@pobox.com> wrote:\n\n> We have been doing this for quite some time, but I just noticed\n> that the 'From' address your MUA uses (\"Christian Couder\n> <christian.couder@gmail.com>\") does not match your Sign-off.  Could\n> you add an in-body 'From:' line if you plan to keep sending your\n> patches from the Gmail address?\n>\n> I suppose nobody has noticed it so far because .mailmap hides the\n> discrepancy once the commit lands.\n\nI hope that this can be resolved by changing my primary email address\nto the gmail one in the .mailmap file as I just requested in this\npatch:\n\nhttps://lore.kernel.org/git/20260803170956.1162626-1-christian.couder@gmail.com/\n\nI will then use \"Signed-off-by: Christian Couder\n<christian.couder@gmail.com>\" and I think everything should work fine.\n"},{"id":"549508","messageId":"CAP8UFD34vCini03OokPT-arcfZRE1KLocb5fVtB3zsHePT2O7Q@mail.gmail.com","threadId":"66022","inReplyTo":"xmqq4ihyehyb.fsf@gitster.g","subject":"Re: [PATCH 7/7] fast-import: use struct option for usage string","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-03T17:23:31Z","receivedAt":"2026-08-03T17:23:44Z","isPatch":true,"body":"On Thu, Jul 16, 2026 at 11:35 PM Junio C Hamano <gitster@pobox.com> wrote:\n\n> OK, I am a bit torn on this.  On one hand:\n>\n>  (1) I do agree that it would be nice to eventually have\n>      fast_import_state_init() (or some other helper that groks\n>      argc/argv) use this options array to parse the command line\n>      arguments.\n>\n>  (2) I am sympathetic to the position that doing so is a bit\n>      outside the scope of this series, whose focus is strictly on\n>      \"git fast-import -h\" and nothing else.\n>\n>  (3) I suspect that when fast_import_state_init() does start using\n>      the options array to initialize the state, the parsed results\n>      will not be stored in the variables this caller currently holds,\n>      but will instead live inside the fast_import_state structure.\n>\n> So in that sense, the huge list of unused function-local variables\n> above are merely throw-away placeholders.  When the real code is\n> written, they will disappear, and the references to them in the\n> fast_import_options[] array will have to be updated to point to\n> members of the structure (or global variables).\n>\n> Still, seeing all of those variables left uninitialized leaves a\n> slightly sour taste.  And because of (3), it would be a clear waste\n> of time to go through the motions of initializing these throw-away\n> locals.\n>\n> Perhaps we would end up in a better position if we bent (2) a bit.\n> After all, my hesitation likely stems from the feeling that this\n> series stops short at a slightly awkward spot, having already\n> completed 90% of the journey.\n>\n> For example, instead of inventing a local, throw-away\n> \"pack_size_limit\" variable, wouldn't it make more sense to refer to\n> the existing global \"max_packsize\" variable from the options[]\n> array below?\n\nYeah, this can work for some variables. But anyway I have tried to\nfully move to using the parse-option API to actually parse the command\nline options, and I hope to send the result in a v2 soon.\n"},{"id":"549552","messageId":"20260804100355.1299498-1-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260716165517.433849-1-christian.couder@gmail.com","subject":"[PATCH v2 00/12] fast-import: standardize usage string and SYNOPSIS","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:43Z","receivedAt":"2026-08-04T10:04:20Z","isPatch":true,"body":"The goal of this series is to improve on `git fast-import`'s usage\nstring as it is obsolete in many ways.\n\nAs it appeared that a good way to reach that goal was to make\n`git fast-import` use the parse-options API, this series also achieves\nthis secondary goal.\n\nAlong the way it modernizes \"builtin/fast-import.c\" mostly by using\n`struct option`, by starting to remove global variables and libify\nthat command, and by introducing a new `OPT_HIDDEN_GROUP` macro.\n\nThere are still many global variables left, so it's left to future\nwork to finish on that direction.\n\nAnyway the usage string is standardized and consistent with the\nSYNOPSIS in the docs, so that the command can be removed from\n\"t/t0450/adoc-help-mismatches\".\n\nUsing the parse-options API also enabled some code standardization and\nsimplification.\n\nOverview of the patches\n=======================\n\n  - Patch 1/12: Introduces OPT_HIDDEN_GROUP and improves on the hidden\n    option tests.\n\n  - Patches 2/12 and 3/12: Improve on the parse-options API docs.\n\n  - Patch 4/12: Cleans up an 'i' loop counter variable in\n    cmd_fast_import().\n\n  - Patches 5/12 and 6/12: Prepare for using the parse-option API to\n    parse command line options.\n\n  - Patches 7/12 and 8/12: Start libifying \"builtin/fast-import.c\" by\n    introducing a 'struct fast_import_state' and using it to store\n    some global variables.\n\n  - Patch 9/12: Improves the usage string and SYNOPSIS by introducing\n    `struct option`.\n\n  - Patch 10/12: Further prepares for using the parse-option API to\n    actually parse command line options by using OPT_CALLBACK.\n\n  - Patch 11/12: Actually parses the command line options using the\n    parse-option API.\n\n  - Patch 12/12: Performs a cleanup of some functions arguments allowed\n    by the previous commit.\n\nChanges since v1\n================\n\nThanks to Junio for reviewing the v1.\n\nThe series has been rebased on top of recent master at 5b2471720c (The\n10th batch, 2026-08-03) to avoid conflicts with recent changes\nespecially cdaf12f762 (parse-options: exit 0 on -h, 2026-07-08).\n\nThe `Signed-off-by:` trailers now use my `christian.couder@gmail.com`\naddress, which is also the address I send patches from, so that they\nmatch. A separate patch updating the `.mailmap` entry to make that\naddress my primary one has been sent separately.\n\n  - Patch 1/12 has been changed by removing `test_must_fail` in front\n    of `test-tool parse-options` when the latter uses `-h` or\n    `--help-all` to adapt this series to the recent changes in master.\n\n  - Patches 5/12 and 6/12 are new and prepare for using the parse-option\n    API to parse command line options.\n\n  - Patch 9/12 (previously 7/7) has been changed a lot so that it\n    reuses existing variables as much as possible instead of\n    introducing a lot of new local variables.\n\n  - Patches 10/12, 11/12 and 12/12 are new and complete the option\n    parsing using the parse-option API.\n\nNote that patch 11/12 changes the behavior of a few undocumented\ncommand line options: `--alias`, `--get-mark`, `--cat-blob`, `--ls`\nand `--notes` are not accepted on the command line anymore. See its\ncommit message for details.\n\nCI tests\n========\n\nThey all pass, except one (Win+Meson test (0) where tests don't want\nto start, so likely unrelated), see:\n\nhttps://github.com/chriscool/git/actions/runs/30894348053\n\n\nChristian Couder (12):\n  parse-options: introduce OPT_HIDDEN_GROUP\n  api-parse-options.adoc: document per-option flags\n  api-parse-options.adoc: document hidden and OPT_*_F option macros\n  fast-import: localize 'i' into the 'for' loops using it\n  fast-import: use int for some bool flags\n  fast-import: factor out option_*() functions\n  fast-import: introduce 'struct fast_import_state'\n  fast-import: move command state globals into 'struct\n    fast_import_state'\n  fast-import: use struct option for usage string\n  fast-import: use callbacks to parse some options\n  fast-import: use parse_options() for command line options\n  fast-import: remove useless from_stream argument\n\n Documentation/git-fast-import.adoc            |   2 +-\n .../technical/api-parse-options.adoc          |  79 +++\n builtin/fast-import.c                         | 577 ++++++++++++------\n parse-options.c                               |   4 +-\n parse-options.h                               |   5 +\n t/helper/test-parse-options.c                 |   4 +\n t/t0040-parse-options.sh                      |  25 +-\n t/t0450/adoc-help-mismatches                  |   1 -\n t/t9300-fast-import.sh                        |   7 +\n 9 files changed, 517 insertions(+), 187 deletions(-)\n\nRange-diff against v1:\n 1:  ba4821276c !  1:  b09d727e71 parse-options: introduce OPT_HIDDEN_GROUP\n    @@ Commit message\n         To test this new macro, let's also improve `test-tool parse-options`\n         and test its output with `--help-all`.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## parse-options.c ##\n     @@ parse-options.c: static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n    @@ t/t0040-parse-options.sh: Alias\n     +\n      test_expect_success 'test help' '\n     +\tcat expect-part1 expect-part2 >expect &&\n    - \ttest_must_fail test-tool parse-options -h >output 2>output.err &&\n    + \ttest-tool parse-options -h >output 2>output.err &&\n      \ttest_must_be_empty output.err &&\n      \ttest_cmp expect output\n      '\n      \n     +test_expect_success 'test --help-all shows hidden group and options' '\n     +\tcat expect-part1 expect-noop expect-part2 expect-hidden >expect-help-all &&\n    -+\ttest_must_fail test-tool parse-options --help-all >output 2>output.err &&\n    ++\ttest-tool parse-options --help-all >output 2>output.err &&\n     +\ttest_must_be_empty output.err &&\n     +\ttest_cmp expect-help-all output\n     +'\n 2:  2690812694 !  2:  fe21471420 api-parse-options.adoc: document per-option flags\n    @@ Commit message\n         parse_options() flags described earlier, and let's describe the `-h`\n         versus `--help-all` behavior for `PARSE_OPT_HIDDEN`.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## Documentation/technical/api-parse-options.adoc ##\n     @@ Documentation/technical/api-parse-options.adoc: Data Structure\n 3:  cf4b276019 !  3:  f43d4cefa4 api-parse-options.adoc: document hidden and OPT_*_F option macros\n    @@ Commit message\n     \n           - Document `OPT_HIDDEN_BOOL()` right after `OPT_BOOL()`.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## Documentation/technical/api-parse-options.adoc ##\n     @@ Documentation/technical/api-parse-options.adoc: Macros\n 4:  9b051efd9f !  4:  3eb8106ed6 fast-import: localize 'i' into the 'for' loops using it\n    @@ Commit message\n         To give each loop counter the type matching its bound\n         (int/unsigned/size_t), let's localize 'i' into each loop that uses it.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n -:  ---------- >  5:  34514eb2ac fast-import: use int for some bool flags\n -:  ---------- >  6:  336395c70a fast-import: factor out option_*() functions\n 5:  2ca7ccafc6 !  7:  45cc2107e2 fast-import: introduce 'struct fast_import_state'\n    @@ Commit message\n         This is a mostly mechanical refactoring with no intended behavior\n         change.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: static int failure;\n      static FILE *pack_edges;\n    - static unsigned int show_stats = 1;\n    - static unsigned int quiet;\n    + static int show_stats = 1;\n    + static int quiet;\n     -static int global_argc;\n     -static const char **global_argv;\n     -static const char *global_prefix;\n    @@ builtin/fast-import.c: static void option_rewrite_submodules(const char *arg, st\n      \tfp = fopen(f, \"r\");\n      \tif (!fp)\n      \t\tdie_errno(_(\"cannot read '%s'\"), f);\n    -@@ builtin/fast-import.c: static void option_rewrite_submodules(const char *arg, struct string_list *list)\n    - \tfree(s);\n    +@@ builtin/fast-import.c: static void option_quiet(void)\n    + \tquiet = 1;\n      }\n      \n     -static int parse_one_option(const char *option)\n     +static int parse_one_option(struct fast_import_state *state, const char *option)\n      {\n      \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n    - \t\tunsigned long v;\n    + \t\toption_max_pack_size(option);\n     @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n      \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n      \t\toption_active_branches(option);\n    @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n     -\t\toption_export_pack_edges(option);\n     +\t\toption_export_pack_edges(state, option);\n      \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n    - \t\tif (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))\n    - \t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n    + \t\toption_signed_commits(option);\n    + \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n     @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n      \treturn 1;\n      }\n 6:  bf25bb3253 !  8:  7f068facc2 fast-import: move command state globals into 'struct fast_import_state'\n    @@ Commit message\n     \n         This is a mechanical refactoring with no intended behavior change.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: static struct recent_command *rc_free;\n 7:  9120d2b5c0 !  9:  3d6ab86518 fast-import: use struct option for usage string\n    @@ Commit message\n         This deliberately leaves it to future work to also use the\n         parse-options API to actually parse the options.\n     \n    -    Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n    +    Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n      ## Documentation/git-fast-import.adoc ##\n     @@ Documentation/git-fast-import.adoc: git-fast-import - Backend for fast Git data importers\n    @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n      \tstruct fast_import_state state;\n      \n     -\tshow_usage_if_asked(argc, argv, fast_import_usage);\n    -+\tunsigned long pack_size_limit, big_file_threshold, depth, active_branches;\n    -+\tchar *edges, *signed_commits, *signed_tags, *date_format, *import_marks;\n    -+\tchar *import_marks_if_exists, *export_marks, *submodules_from, *submodules_to;\n    -+\tint opt_quiet, opt_show_stats, opt_relative_marks, opt_force, opt_done;\n    -+\tint opt_allow_unsafe;\n    -+\tint cat_blob;\n    ++\tunsigned long pack_size_limit, big_file_threshold;\n    ++\tchar *edges, *signed_commits, *signed_tags, *date_format;\n    ++\tchar *import_marks_if_exists, *submodules_from, *submodules_to;\n      \n     -\tfast_import_state_init(&state, argc, argv, prefix);\n     +\t/*\n    @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n     +\t\tOPT_GROUP(N_(\"Common\")),\n     +\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n     +\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n    -+\t\tOPT_BOOL_F(0, \"stats\", &opt_show_stats,\n    ++\t\tOPT_BOOL_F(0, \"stats\", &show_stats,\n     +\t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n     +\t\t\t   PARSE_OPT_NONEG),\n    -+\t\tOPT_BOOL_F(0, \"quiet\", &opt_quiet,\n    ++\t\tOPT_BOOL_F(0, \"quiet\", &quiet,\n     +\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n    -+\t\tOPT_BOOL_F(0, \"force\", &opt_force,\n    ++\t\tOPT_BOOL_F(0, \"force\", &force_update,\n     +\t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n    -+\t\tOPT_BOOL_F(0, \"done\", &opt_done,\n    ++\t\tOPT_BOOL_F(0, \"done\", &require_explicit_termination,\n     +\t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n     +\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n     +\t\t\t     N_(\"maximum size of each output pack file\")),\n     +\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n     +\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n    -+\t\tOPT_UNSIGNED(0, \"depth\", &depth,\n    ++\t\tOPT_UNSIGNED(0, \"depth\", &max_depth,\n     +\t\t\t     N_(\"maximum delta depth\")),\n    -+\t\tOPT_UNSIGNED(0, \"active-branches\", &active_branches,\n    ++\t\tOPT_UNSIGNED(0, \"active-branches\", &max_active_branches,\n     +\t\t\t     N_(\"maximum number of branches to maintain active\")),\n     +\t\tOPT_GROUP(N_(\"Marks\")),\n    -+\t\tOPT_STRING_F(0, \"import-marks\", &import_marks, N_(\"file\"),\n    ++\t\tOPT_STRING_F(0, \"import-marks\", &import_marks_file, N_(\"file\"),\n     +\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n     +\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n     +\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n    -+\t\tOPT_STRING_F(0, \"export-marks\", &export_marks, N_(\"file\"),\n    ++\t\tOPT_STRING_F(0, \"export-marks\", &export_marks_file, N_(\"file\"),\n     +\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n    -+\t\tOPT_BOOL(0, \"relative-marks\", &opt_relative_marks,\n    ++\t\tOPT_BOOL(0, \"relative-marks\", &relative_marks_paths,\n     +\t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n     +\t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n     +\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n    @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n     +\t\t\t     N_(\"how to handle signed tags\"),\n     +\t\t\t     PARSE_OPT_NONEG),\n     +\t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n    -+\t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &opt_allow_unsafe,\n    ++\t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &state.allow_unsafe_features,\n     +\t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n     +\t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n     +\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n     +\t\t\t     N_(\"dump edge commits to <file>\"),\n     +\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n    -+\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob,\n    ++\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob_fd,\n     +\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n     +\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n     +\t\tOPT_END()\n    @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n      \tgit_pack_config();\n     \n      ## t/t0450/adoc-help-mismatches ##\n    -@@ t/t0450/adoc-help-mismatches: credential\n    +@@ t/t0450/adoc-help-mismatches: column\n    + credential\n      credential-cache\n      credential-store\n    - fast-export\n     -fast-import\n      fetch-pack\n      fmt-merge-msg\n -:  ---------- > 10:  3208937f13 fast-import: use callbacks to parse some options\n -:  ---------- > 11:  202a50beec fast-import: use parse_options() for command line options\n -:  ---------- > 12:  99ff791a62 fast-import: remove useless from_stream argument\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549553","messageId":"20260804100355.1299498-2-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 01/12] parse-options: introduce OPT_HIDDEN_GROUP","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:44Z","receivedAt":"2026-08-04T10:04:22Z","isPatch":true,"body":"Hidden options are not shown by `git <cmd> -h`, but are still shown by\n`git <cmd> --help-all`. If there are a lot of hidden options or if they\ndon't belong to the same categories as other options, there is\ncurrently no way to properly group them.\n\nUsing `OPT_GROUP(\"Foo\")` means that \"Foo\" will always be shown which we\ndon't want if that group contains only hidden options.\n\nTo provide a way to have groups shown only when hidden options are\nshown, let's implement an OPT_HIDDEN_GROUP macro.\n\nTo test this new macro, let's also improve `test-tool parse-options`\nand test its output with `--help-all`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n parse-options.c               |  4 ++--\n parse-options.h               |  5 +++++\n t/helper/test-parse-options.c |  4 ++++\n t/t0040-parse-options.sh      | 25 ++++++++++++++++++++++++-\n 4 files changed, 35 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 08c21d9fc0..4519ead9dc 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1414,6 +1414,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \n \t\tif (opts->type == OPTION_SUBCOMMAND)\n \t\t\tcontinue;\n+\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n+\t\t\tcontinue;\n \t\tif (opts->type == OPTION_GROUP) {\n \t\t\tfputc('\\n', outfile);\n \t\t\tneed_newline = 0;\n@@ -1421,8 +1423,6 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t\t\t\tfprintf(outfile, \"%s\\n\", _(opts->help));\n \t\t\tcontinue;\n \t\t}\n-\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n-\t\t\tcontinue;\n \n \t\tif (need_newline) {\n \t\t\tfputc('\\n', outfile);\ndiff --git a/parse-options.h b/parse-options.h\nindex 3ec8ba5cc8..d7f896a933 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -237,6 +237,11 @@ struct option {\n \t.type = OPTION_GROUP, \\\n \t.help = (h), \\\n }\n+#define OPT_HIDDEN_GROUP(h) { \\\n+\t.type = OPTION_GROUP, \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_HIDDEN, \\\n+}\n #define OPT_BIT(s, l, v, h, b)      OPT_BIT_F(s, l, v, h, b, 0)\n #define OPT_BITOP(s, l, v, h, set, clear) { \\\n \t.type = OPTION_BITOP, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 68579d83f3..f181f0c02d 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -209,6 +209,10 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Alias\"),\n \t\tOPT_STRING('A', \"alias-source\", &string, \"string\", \"get a string\"),\n \t\tOPT_ALIAS('Z', \"alias-target\", \"alias-source\"),\n+\t\tOPT_HIDDEN_GROUP(\"Hidden options\"),\n+\t\tOPT_HIDDEN_BOOL(0, \"hidden-bool\", &boolean, \"get a boolean\"),\n+\t\tOPT_INTEGER_F('k', \"hidden-integer\", &integer, \"get a integer\",\n+\t\t\t      PARSE_OPT_HIDDEN),\n \t\tOPT_END(),\n \t};\n \tint ret = 0;\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex a22533f9ed..449fff4d34 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -7,7 +7,7 @@ test_description='our own option parser'\n \n . ./test-lib.sh\n \n-cat >expect <<\\EOF\n+cat >expect-part1 <<\\EOF\n usage: test-tool parse-options <options>\n \n     A helper function for the parse-options API.\n@@ -41,6 +41,9 @@ String options\n     --[no-]string2 <str>  get another string\n     --[no-]st <st>        get another string (pervert ordering)\n     -o <str>              get another string\n+EOF\n+\n+cat >expect-part2 <<\\EOF\n     --longhelp            help text of this entry\n                           spans multiple lines\n     --[no-]list <str>     add str to list\n@@ -67,12 +70,32 @@ Alias\n \n EOF\n \n+cat >expect-noop <<\\EOF\n+    --[no-]obsolete       no-op (backward compatibility)\n+EOF\n+\n+cat >expect-hidden <<\\EOF\n+Hidden options\n+    --[no-]hidden-bool    get a boolean\n+    -k, --[no-]hidden-integer <n>\n+                          get a integer\n+\n+EOF\n+\n test_expect_success 'test help' '\n+\tcat expect-part1 expect-part2 >expect &&\n \ttest-tool parse-options -h >output 2>output.err &&\n \ttest_must_be_empty output.err &&\n \ttest_cmp expect output\n '\n \n+test_expect_success 'test --help-all shows hidden group and options' '\n+\tcat expect-part1 expect-noop expect-part2 expect-hidden >expect-help-all &&\n+\ttest-tool parse-options --help-all >output 2>output.err &&\n+\ttest_must_be_empty output.err &&\n+\ttest_cmp expect-help-all output\n+'\n+\n mv expect expect.err\n \n check () {\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549554","messageId":"20260804100355.1299498-3-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 02/12] api-parse-options.adoc: document per-option flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:45Z","receivedAt":"2026-08-04T10:04:23Z","isPatch":true,"body":"The \"Flags\" section in \"Documentation/technical/api-parse-options.adoc\"\ndocuments the flags that can be passed to parse_options() itself. It\ndoes not, however, document the flags that can be set on individual\noptions through the `flags` member of `struct option` (and through the\n`OPT_*_F()` macro variants).\n\nThese per-option flags are used throughout the codebase (for example\n`PARSE_OPT_HIDDEN` is used to hide an option from `-h` while still\nshowing it with `--help-all`), but a reader currently has to dig into\n\"parse-options.h\" to find them.\n\nTo remediate that, let's add an \"Option flags\" subsection to the\n\"Data Structure\" section, just before the list of option macros.\n\nLet's also make it explicit that these are distinct from the\nparse_options() flags described earlier, and let's describe the `-h`\nversus `--help-all` behavior for `PARSE_OPT_HIDDEN`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n .../technical/api-parse-options.adoc          | 61 +++++++++++++++++++\n 1 file changed, 61 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex 880eb94642..fb4580e755 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -150,6 +150,67 @@ Data Structure\n \n The main data structure is an array of the `option` struct,\n say `static struct option builtin_add_options[]`.\n+\n+Option flags\n+~~~~~~~~~~~~\n+\n+Each option can carry flags in the `flags` field of its `option`\n+struct. These are per-option flags and are distinct from the\n+`parse_options()` flags described above; they are usually set through\n+the `OPT_*_F()` macro variants (see below) rather than by hand. They\n+are the bitwise-or of:\n+\n+`PARSE_OPT_OPTARG`::\n+\tThe option's argument is optional, i.e. both `--option` and\n+\t`--option=<value>` are accepted.\n+\n+`PARSE_OPT_NOARG`::\n+\tThe option takes no argument at all. Using `--option=<value>`\n+\tis rejected.\n+\n+`PARSE_OPT_NONEG`::\n+\tDisable the automatically generated negated `--no-option`\n+\tform.\n+\n+`PARSE_OPT_HIDDEN`::\n+\tHide the option: it is omitted from the usage shown by\n+\t`git <cmd> -h`, but is still shown by `git <cmd> --help-all`.\n+\tThe option is parsed as usual either way. This is meant for\n+\tdeprecated, advanced or otherwise uncommon options.\n+\n+`PARSE_OPT_LASTARG_DEFAULT`::\n+\tUse the default value (`defval`) when the option is used\n+\twithout an argument, even for an option that normally requires\n+\tone. Only the last argument on the command line takes effect.\n+\n+`PARSE_OPT_NODASH`::\n+\tThe option is a single character without a leading dash, such\n+\tas the `+` used by some commands.\n+\n+`PARSE_OPT_LITERAL_ARGHELP`::\n+\tUse the argument help string (`argh`) verbatim in the usage\n+\toutput instead of surrounding it with `<>` or `[]`. Useful when\n+\t`argh` already contains a hand-formatted description.\n+\n+`PARSE_OPT_FROM_ALIAS`::\n+\tInternal flag, set on options that were expanded from a\n+\tconfigured alias. It should not be set by callers.\n+\n+`PARSE_OPT_NOCOMPLETE`::\n+\tDo not offer this option for completion.\n+\n+`PARSE_OPT_COMP_ARG`::\n+\tThe option's argument, rather than the option itself, is what\n+\tshould be completed.\n+\n+`PARSE_OPT_CMDMODE`::\n+\tThe option is one of several mutually exclusive \"command mode\"\n+\toptions that share the same variable. Using more than one of\n+\tthem at once is rejected.\n+\n+Macros\n+~~~~~~\n+\n There are some macros to easily define options:\n \n `OPT__ABBREV(&int_var)`::\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549555","messageId":"20260804100355.1299498-4-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 03/12] api-parse-options.adoc: document hidden and OPT_*_F option macros","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:46Z","receivedAt":"2026-08-04T10:04:24Z","isPatch":true,"body":"In \"Documentation/technical/api-parse-options.adoc\", the list of option\nmacros does not mention the `OPT_*_F()` macro variants that take a\ntrailing `flags` argument, nor the `OPT_HIDDEN_GROUP()` and\n`OPT_HIDDEN_BOOL()` convenience macros.\n\nNow that a previous commit documents the per-option flags, let's\ndocument these macros too:\n\n  - Add a paragraph explaining the `OPT_*_F` convention and how it\n    relates to the per-option flags.\n\n  - Document `OPT_HIDDEN_GROUP()`, introduced in a previous commit,\n    right after `OPT_GROUP()`.\n\n  - Document `OPT_HIDDEN_BOOL()` right after `OPT_BOOL()`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n Documentation/technical/api-parse-options.adoc | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex fb4580e755..0e10327d07 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -213,6 +213,13 @@ Macros\n \n There are some macros to easily define options:\n \n+Many of the macros below have an `_F` variant (for example `OPT_BOOL_F`,\n+`OPT_STRING_F`, `OPT_INTEGER_F`, `OPT_SET_INT_F`, `OPT_BIT_F` and\n+`OPT_CALLBACK_F`) that takes an additional trailing `flags` argument.\n+That argument is the bitwise-or of the per-option flags described in the\n+\"Option flags\" section above; the non-`_F` macros are simply defined\n+with `flags` set to `0`.\n+\n `OPT__ABBREV(&int_var)`::\n \tAdd `--abbrev[=<n>]`.\n \n@@ -236,10 +243,21 @@ There are some macros to easily define options:\n \tdescribes the group or an empty string.\n \tStart the description with an upper-case letter.\n \n+`OPT_HIDDEN_GROUP(description)`::\n+\tLike `OPT_GROUP()`, but the group header carries\n+\t`PARSE_OPT_HIDDEN`, so it is only shown by `--help-all` and not\n+\tby `-h`. Use it to label a group that contains only hidden\n+\toptions, which would otherwise show an empty header under `-h`.\n+\n `OPT_BOOL(short, long, &int_var, description)`::\n \tIntroduce a boolean option. `int_var` is set to one with\n \t`--option` and set to zero with `--no-option`.\n \n+`OPT_HIDDEN_BOOL(short, long, &int_var, description)`::\n+\tLike `OPT_BOOL()`, but the option carries `PARSE_OPT_HIDDEN`,\n+\tso it is hidden from `-h` while still being shown by\n+\t`--help-all`.\n+\n `OPT_COUNTUP(short, long, &int_var, description)`::\n \tIntroduce a count-up option.\n \tEach use of `--option` increments `int_var`, starting from zero\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549556","messageId":"20260804100355.1299498-5-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 04/12] fast-import: localize 'i' into the 'for' loops using it","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:47Z","receivedAt":"2026-08-04T10:04:26Z","isPatch":true,"body":"In cmd_fast_import(), a\tlocal variable 'i' is defined as an\n`unsigned int` and then used as a loop counter in four different\n`for (i = ...; i < ...; i++) { ... }` loops.\n\nBut in three out of the four cases, `unsigned int` isn't the best type\nto use.\n\nTo give each loop counter the type matching its bound\n(int/unsigned/size_t), let's localize 'i' into each loop that uses it.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 10 ++++------\n 1 file changed, 4 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 6692f7cd81..9fc9ebe65a 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3937,8 +3937,6 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n-\tunsigned int i;\n-\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3959,7 +3957,7 @@ int cmd_fast_import(int argc,\n \t * line to override stream data). But we must do an early parse of any\n \t * command-line options that impact how we interpret the feature lines.\n \t */\n-\tfor (i = 1; i < argc; i++) {\n+\tfor (int i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n@@ -3972,7 +3970,7 @@ int cmd_fast_import(int argc,\n \tglobal_prefix = prefix;\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n-\tfor (i = 0; i < (cmd_save - 1); i++)\n+\tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n \trc_free[cmd_save - 1].next = NULL;\n \n@@ -4035,9 +4033,9 @@ int cmd_fast_import(int argc,\n \n \tif (show_stats) {\n \t\tuintmax_t total_count = 0, duplicate_count = 0;\n-\t\tfor (i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n \t\t\ttotal_count += object_count_by_type[i];\n-\t\tfor (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n \t\t\tduplicate_count += duplicate_count_by_type[i];\n \n \t\tfprintf(stderr, \"%s statistics:\\n\", argv[0]);\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549557","messageId":"20260804100355.1299498-6-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 05/12] fast-import: use int for some bool flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:48Z","receivedAt":"2026-08-04T10:04:26Z","isPatch":true,"body":"The `show_stats` and `quiet` flags are meant to be parsed and used as\nboolean flags.\n\nTo easily parse them using OPT_BOOL in a following commit, let's change\ntheir type from 'unsigned int' to just 'int'.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 9fc9ebe65a..9c8edd7c89 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -182,8 +182,8 @@ static unsigned long branch_count;\n static unsigned long branch_load_count;\n static int failure;\n static FILE *pack_edges;\n-static unsigned int show_stats = 1;\n-static unsigned int quiet;\n+static int show_stats = 1;\n+static int quiet;\n static int global_argc;\n static const char **global_argv;\n static const char *global_prefix;\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549558","messageId":"20260804100355.1299498-7-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 06/12] fast-import: factor out option_*() functions","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:49Z","receivedAt":"2026-08-04T10:04:27Z","isPatch":true,"body":"In a following commit we are going to use the parse-options API to\nstart parsing options. Some options will have to be parsed using\nOPT_CALLBACK as they process their arguments in special ways.\n\nWhen the processing code is already factored out in an option_*()\nfunction, like for `--date-format`, we can reuse that function.\nUnfortunately for other options the processing code has not been\nfactored out yet.\n\nLet's do it now and factor out the code that handles the following\noptions:\n\n  - `--max-pack-size=<n>`\n  - `--big-file-threshold=<n>`\n  - `--signed-commits=<mode>`\n  - `--signed-tags=<mode>`\n  - `--quiet`\n\ninto new option_*() functions:\n\n  - option_max_pack_size()\n  - option_big_file_threshold()\n  - option_signed_commits()\n  - option_signed_tags()\n  - option_quiet()\n\nso that we can reuse these functions in following commits when the\nparse-option API will be used.\n\nNote that there are some behavior changes as we now die() with a\nproper error message when git_parse_ulong() cannot parse the argument\nfrom --max-pack-size or from --big-file-threshold. Previously we would\nend up calling die(\"unknown option\") instead.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 69 ++++++++++++++++++++++++++++++-------------\n 1 file changed, 48 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 9c8edd7c89..a6e3cc0033 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3751,25 +3751,55 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tfree(s);\n }\n \n+static void option_max_pack_size(const char *arg)\n+{\n+\tunsigned long v;\n+\n+\tif (!git_parse_ulong(arg, &v))\n+\t\tdie(_(\"--max-pack-size: argument must be a non-negative integer\"));\n+\tif (v < 8192) {\n+\t\twarning(_(\"max-pack-size is now in bytes, assuming --max-pack-size=%lum\"), v);\n+\t\tv *= 1024 * 1024;\n+\t} else if (v < 1024 * 1024) {\n+\t\twarning(_(\"minimum max-pack-size is 1 MiB\"));\n+\t\tv = 1024 * 1024;\n+\t}\n+\tmax_packsize = v;\n+}\n+\n+static void option_big_file_threshold(const char *arg)\n+{\n+\tunsigned long v;\n+\n+\tif (!git_parse_ulong(arg, &v))\n+\t\tdie(_(\"--big-file-threshold: argument must be a non-negative integer\"));\n+\trepo_settings_set_big_file_threshold(the_repository, v);\n+}\n+\n+static void option_signed_commits(const char *arg)\n+{\n+\tif (parse_sign_mode(arg, &signed_commit_mode, &signed_commit_keyid))\n+\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), arg);\n+}\n+\n+static void option_signed_tags(const char *arg)\n+{\n+\tif (parse_sign_mode(arg, &signed_tag_mode, &signed_tag_keyid))\n+\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), arg);\n+}\n+\n+static void option_quiet(void)\n+{\n+\tshow_stats = 0;\n+\tquiet = 1;\n+}\n+\n static int parse_one_option(const char *option)\n {\n \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n-\t\tunsigned long v;\n-\t\tif (!git_parse_ulong(option, &v))\n-\t\t\treturn 0;\n-\t\tif (v < 8192) {\n-\t\t\twarning(_(\"max-pack-size is now in bytes, assuming --max-pack-size=%lum\"), v);\n-\t\t\tv *= 1024 * 1024;\n-\t\t} else if (v < 1024 * 1024) {\n-\t\t\twarning(_(\"minimum max-pack-size is 1 MiB\"));\n-\t\t\tv = 1024 * 1024;\n-\t\t}\n-\t\tmax_packsize = v;\n+\t\toption_max_pack_size(option);\n \t} else if (skip_prefix(option, \"big-file-threshold=\", &option)) {\n-\t\tunsigned long v;\n-\t\tif (!git_parse_ulong(option, &v))\n-\t\t\treturn 0;\n-\t\trepo_settings_set_big_file_threshold(the_repository, v);\n+\t\toption_big_file_threshold(option);\n \t} else if (skip_prefix(option, \"depth=\", &option)) {\n \t\toption_depth(option);\n \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n@@ -3777,14 +3807,11 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))\n-\t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n+\t\toption_signed_commits(option);\n \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_tag_mode, &signed_tag_keyid))\n-\t\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), option);\n+\t\toption_signed_tags(option);\n \t} else if (!strcmp(option, \"quiet\")) {\n-\t\tshow_stats = 0;\n-\t\tquiet = 1;\n+\t\toption_quiet();\n \t} else if (!strcmp(option, \"stats\")) {\n \t\tshow_stats = 1;\n \t} else if (!strcmp(option, \"allow-unsafe-features\")) {\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549559","messageId":"20260804100355.1299498-8-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 07/12] fast-import: introduce 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:50Z","receivedAt":"2026-08-04T10:04:29Z","isPatch":true,"body":"\"builtin/fast-import.c\" uses a large number of global variables. This\nmakes it harder than necessary to reason about and improve. Especially\nadding new features requires adding more global variables, while\nmodernizing and eventually libifying the code becomes more and more\ndifficult.\n\nTo start reverting the sad trend to more and more globals and to start\ncleaning things up, let's introduce a 'struct fast_import_state' and\npass an instance of it as the first argument to many functions.\n\nThis is similar to what was done for \"builtin/apply.c\" by introducing a\n'struct apply_state', see 07d7e290ff (apply: move 'struct apply_state'\nto a header file, 2016-08-11) and related commits.\n\nAs a first step only the 'global_argc', 'global_argv' and\n'global_prefix' variables are moved into the new struct. More variables\nwill be moved into it in the following commits.\n\nSome functions receive the new 'state' parameter only to pass it\nalong or for future use, so they are marked with UNUSED for now to\nsatisfy '-Werror=unused-parameter'.\n\nThis is a mostly mechanical refactoring with no intended behavior\nchange.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 262 ++++++++++++++++++++++--------------------\n 1 file changed, 138 insertions(+), 124 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex a6e3cc0033..cb0c856e41 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -184,10 +184,6 @@ static int failure;\n static FILE *pack_edges;\n static int show_stats = 1;\n static int quiet;\n-static int global_argc;\n-static const char **global_argv;\n-static const char *global_prefix;\n-\n static enum sign_mode signed_tag_mode = SIGN_VERBATIM;\n static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n static const char *signed_commit_keyid;\n@@ -276,10 +272,27 @@ static kh_oid_map_t *sub_oid_map;\n /* Where to write output of cat-blob commands */\n static int cat_blob_fd = STDOUT_FILENO;\n \n-static void parse_argv(void);\n-static void parse_get_mark(const char *p);\n-static void parse_cat_blob(const char *p);\n-static void parse_ls(const char *p, struct branch *b);\n+/* Command state */\n+struct fast_import_state {\n+\tint argc;\n+\tconst char **argv;\n+\tconst char *prefix;\n+};\n+\n+static void fast_import_state_init(struct fast_import_state *state,\n+\t\t\t\t   int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n+{\n+\tmemset(state, 0, sizeof(*state));\n+\tstate->argc = argc;\n+\tstate->argv = argv;\n+\tstate->prefix = prefix;\n+}\n+\n+static void parse_argv(struct fast_import_state *state);\n+static void parse_get_mark(struct fast_import_state *state, const char *p);\n+static void parse_cat_blob(struct fast_import_state *state, const char *p);\n+static void parse_ls(struct fast_import_state *state, const char *p, struct branch *b);\n \n static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p)\n {\n@@ -1845,7 +1858,7 @@ static void read_marks(void)\n }\n \n \n-static int read_next_command(void)\n+static int read_next_command(struct fast_import_state *state)\n {\n \tstatic int stdin_eof = 0;\n \n@@ -1867,7 +1880,7 @@ static int read_next_command(void)\n \t\t\tif (!seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n-\t\t\t\tparse_argv();\n+\t\t\t\tparse_argv(state);\n \t\t\t}\n \n \t\t\trc = rc_free;\n@@ -1899,22 +1912,22 @@ static void skip_optional_lf(void)\n \t\tungetc(term_char, stdin);\n }\n \n-static void parse_mark(void)\n+static void parse_mark(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"mark :\", &v)) {\n \t\tnext_mark = strtoumax(v, NULL, 10);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \telse\n \t\tnext_mark = 0;\n }\n \n-static void parse_original_identifier(void)\n+static void parse_original_identifier(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"original-oid \", &v))\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n }\n \n static int parse_data(struct strbuf *sb, uintmax_t limit, uintmax_t *len_res)\n@@ -2068,11 +2081,11 @@ static void parse_and_store_blob(\n \t}\n }\n \n-static void parse_new_blob(void)\n+static void parse_new_blob(struct fast_import_state *state)\n {\n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tparse_and_store_blob(&last_blob, NULL, next_mark);\n }\n \n@@ -2368,7 +2381,7 @@ static void parse_path_space(struct strbuf *sb, const char *p,\n \t(*endp)++;\n }\n \n-static void file_change_m(const char *p, struct branch *b)\n+static void file_change_m(struct fast_import_state *state, const char *p, struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct object_entry *oe;\n@@ -2435,10 +2448,10 @@ static void file_change_m(const char *p, struct branch *b)\n \t\tif (S_ISDIR(mode))\n \t\t\tdie(_(\"directories cannot be specified 'inline': %s\"),\n \t\t\t\tcommand_buf.buf);\n-\t\twhile (read_next_command() != EOF) {\n+\t\twhile (read_next_command(state) != EOF) {\n \t\t\tconst char *v;\n \t\t\tif (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\t\tparse_cat_blob(v);\n+\t\t\t\tparse_cat_blob(state, v);\n \t\t\telse {\n \t\t\t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t\t\t\tbreak;\n@@ -2512,7 +2525,7 @@ static void file_change_cr(const char *p, struct branch *b, int rename)\n \t\tleaf.tree);\n }\n \n-static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n+static void note_change_n(struct fast_import_state *state, const char *p, struct branch *b, unsigned char *old_fanout)\n {\n \tstruct object_entry *oe;\n \tstruct branch *s;\n@@ -2577,7 +2590,7 @@ static void note_change_n(const char *p, struct branch *b, unsigned char *old_fa\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), p);\n \n \tif (inline_data) {\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t} else if (oe) {\n \t\tif (oe->type != OBJ_BLOB)\n@@ -2644,7 +2657,7 @@ static void parse_from_existing(struct branch *b)\n \t}\n }\n \n-static int parse_objectish(struct branch *b, const char *objectish)\n+static int parse_objectish(struct fast_import_state *state, struct branch *b, const char *objectish)\n {\n \tstruct branch *s;\n \tstruct object_id oid;\n@@ -2687,31 +2700,31 @@ static int parse_objectish(struct branch *b, const char *objectish)\n \t\tb->branch_tree.tree = NULL;\n \t}\n \n-\tread_next_command();\n+\tread_next_command(state);\n \treturn 1;\n }\n \n-static int parse_from(struct branch *b)\n+static int parse_from(struct fast_import_state *state, struct branch *b)\n {\n \tconst char *from;\n \n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, from);\n+\treturn parse_objectish(state, b, from);\n }\n \n-static int parse_objectish_with_prefix(struct branch *b, const char *prefix)\n+static int parse_objectish_with_prefix(struct fast_import_state *state, struct branch *b, const char *prefix)\n {\n \tconst char *base;\n \n \tif (!skip_prefix(command_buf.buf, prefix, &base))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, base);\n+\treturn parse_objectish(state, b, base);\n }\n \n-static struct hash_list *parse_merge(unsigned int *count)\n+static struct hash_list *parse_merge(struct fast_import_state *state, unsigned int *count)\n {\n \tstruct hash_list *list = NULL, **tail = &list, *n;\n \tconst char *from;\n@@ -2745,7 +2758,7 @@ static struct hash_list *parse_merge(unsigned int *count)\n \t\ttail = &n->next;\n \n \t\t(*count)++;\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \treturn list;\n }\n@@ -2756,7 +2769,7 @@ struct signature_data {\n \tstruct strbuf data;   /* The actual signature data */\n };\n \n-static void parse_one_signature(struct signature_data *sig, const char *v)\n+static void parse_one_signature(struct fast_import_state *state, struct signature_data *sig, const char *v)\n {\n \tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n \tchar *space = strchr(args, ' ');\n@@ -2781,15 +2794,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v)\n \t\twarning(_(\"'unknown' signature format in gpgsig\"));\n \n \t/* Read signature data */\n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&sig->data, 0, NULL);\n }\n \n-static void discard_one_signature(void)\n+static void discard_one_signature(struct fast_import_state *state)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&data, 0, NULL);\n \tstrbuf_release(&data);\n }\n@@ -2827,13 +2840,14 @@ static void store_signature(struct signature_data *stored_sig,\n \t}\n }\n \n-static void import_one_signature(struct signature_data *sig_sha1,\n+static void import_one_signature(struct fast_import_state *state,\n+\t\t\t\t struct signature_data *sig_sha1,\n \t\t\t\t struct signature_data *sig_sha256,\n \t\t\t\t const char *v)\n {\n \tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n \n-\tparse_one_signature(&sig, v);\n+\tparse_one_signature(state, &sig, v);\n \n \tif (!strcmp(sig.hash_algo, \"sha1\"))\n \t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n@@ -2947,7 +2961,7 @@ static void handle_signature_if_invalid(struct strbuf *new_data,\n \tstrbuf_release(&tmp_buf);\n }\n \n-static void parse_new_commit(const char *arg)\n+static void parse_new_commit(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n@@ -2965,16 +2979,16 @@ static void parse_new_commit(const char *arg)\n \tif (!b)\n \t\tb = new_branch(arg);\n \n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tif (skip_prefix(command_buf.buf, \"author \", &v)) {\n \t\tauthor = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (skip_prefix(command_buf.buf, \"committer \", &v)) {\n \t\tcommitter = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (!committer)\n \t\tdie(_(\"expected committer but didn't get one\"));\n@@ -2990,7 +3004,7 @@ static void parse_new_commit(const char *arg)\n \t\t\twarning(_(\"stripping a commit signature\"));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n-\t\t\tdiscard_one_signature();\n+\t\t\tdiscard_one_signature(state);\n \t\t\tbreak;\n \n \t\t/* Second, modes that parse the signature */\n@@ -3001,24 +3015,24 @@ static void parse_new_commit(const char *arg)\n \t\tcase SIGN_STRIP_IF_INVALID:\n \t\tcase SIGN_SIGN_IF_INVALID:\n \t\tcase SIGN_ABORT_IF_INVALID:\n-\t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n+\t\t\timport_one_signature(state, &sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n \t\t/* Third, BUG */\n \t\tdefault:\n \t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n \t\t}\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tparse_data(&msg, 0, NULL);\n-\tread_next_command();\n-\tparse_from(b);\n-\tmerge_list = parse_merge(&merge_count);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n+\tmerge_list = parse_merge(state, &merge_count);\n \n \t/* ensure the branch is active/loaded */\n \tif (!b->branch_tree.tree || !max_active_branches) {\n@@ -3031,7 +3045,7 @@ static void parse_new_commit(const char *arg)\n \t/* file_change* */\n \twhile (command_buf.len > 0) {\n \t\tif (skip_prefix(command_buf.buf, \"M \", &v))\n-\t\t\tfile_change_m(v, b);\n+\t\t\tfile_change_m(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"D \", &v))\n \t\t\tfile_change_d(v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"R \", &v))\n@@ -3039,18 +3053,18 @@ static void parse_new_commit(const char *arg)\n \t\telse if (skip_prefix(command_buf.buf, \"C \", &v))\n \t\t\tfile_change_cr(v, b, 0);\n \t\telse if (skip_prefix(command_buf.buf, \"N \", &v))\n-\t\t\tnote_change_n(v, b, &prev_fanout);\n+\t\t\tnote_change_n(state, v, b, &prev_fanout);\n \t\telse if (!strcmp(\"deleteall\", command_buf.buf))\n \t\t\tfile_change_deleteall(b);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, b);\n+\t\t\tparse_ls(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(state, v);\n \t\telse {\n \t\t\tunread_command_buf = 1;\n \t\t\tbreak;\n \t\t}\n-\t\tif (read_next_command() == EOF)\n+\t\tif (read_next_command(state) == EOF)\n \t\t\tbreak;\n \t}\n \n@@ -3188,7 +3202,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \t}\n }\n \n-static void parse_new_tag(const char *arg)\n+static void parse_new_tag(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tconst char *from;\n@@ -3207,8 +3221,8 @@ static void parse_new_tag(const char *arg)\n \telse\n \t\tfirst_tag = t;\n \tlast_tag = t;\n-\tread_next_command();\n-\tparse_mark();\n+\tread_next_command(state);\n+\tparse_mark(state);\n \n \t/* from ... */\n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n@@ -3236,15 +3250,15 @@ static void parse_new_tag(const char *arg)\n \t\t\ttype = oe->type;\n \t} else\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), from);\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* original-oid ... */\n-\tparse_original_identifier();\n+\tparse_original_identifier(state);\n \n \t/* tagger ... */\n \tif (skip_prefix(command_buf.buf, \"tagger \", &v)) {\n \t\ttagger = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t} else\n \t\ttagger = NULL;\n \n@@ -3275,7 +3289,7 @@ static void parse_new_tag(const char *arg)\n \t\tt->pack_id = pack_id;\n }\n \n-static void parse_reset_branch(const char *arg)\n+static void parse_reset_branch(struct fast_import_state *state, const char *arg)\n {\n \tstruct branch *b;\n \tconst char *tag_name;\n@@ -3292,8 +3306,8 @@ static void parse_reset_branch(const char *arg)\n \t}\n \telse\n \t\tb = new_branch(arg);\n-\tread_next_command();\n-\tparse_from(b);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n \tif (b->delete && skip_prefix(b->name, \"refs/tags/\", &tag_name)) {\n \t\t/*\n \t\t * Elsewhere, we call dump_branches() before dump_tags(),\n@@ -3378,7 +3392,7 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid)\n \t\tfree(buf);\n }\n \n-static void parse_get_mark(const char *p)\n+static void parse_get_mark(struct fast_import_state *state UNUSED, const char *p)\n {\n \tstruct object_entry *oe;\n \tchar output[GIT_MAX_HEXSZ + 2];\n@@ -3395,7 +3409,7 @@ static void parse_get_mark(const char *p)\n \tcat_blob_write(output, the_hash_algo->hexsz + 1);\n }\n \n-static void parse_cat_blob(const char *p)\n+static void parse_cat_blob(struct fast_import_state *state UNUSED, const char *p)\n {\n \tstruct object_entry *oe;\n \tstruct object_id oid;\n@@ -3560,7 +3574,7 @@ static void print_ls(int mode, const unsigned char *hash, const char *path)\n \tcat_blob_write(line.buf, line.len);\n }\n \n-static void parse_ls(const char *p, struct branch *b)\n+static void parse_ls(struct fast_import_state *state UNUSED, const char *p, struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct tree_entry *root = NULL;\n@@ -3607,13 +3621,13 @@ static void checkpoint(void)\n \tdump_marks();\n }\n \n-static void parse_checkpoint(void)\n+static void parse_checkpoint(struct fast_import_state *state UNUSED)\n {\n \tcheckpoint_requested = 1;\n \tskip_optional_lf();\n }\n \n-static void parse_progress(void)\n+static void parse_progress(struct fast_import_state *state UNUSED)\n {\n \tfwrite(command_buf.buf, 1, command_buf.len, stdout);\n \tfputc('\\n', stdout);\n@@ -3621,36 +3635,36 @@ static void parse_progress(void)\n \tskip_optional_lf();\n }\n \n-static void parse_alias(void)\n+static void parse_alias(struct fast_import_state *state)\n {\n \tstruct object_entry *e;\n \tstruct branch b;\n \n \tskip_optional_lf();\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* mark ... */\n-\tparse_mark();\n+\tparse_mark(state);\n \tif (!next_mark)\n \t\tdie(_(\"expected 'mark' command, got %s\"), command_buf.buf);\n \n \t/* to ... */\n \tmemset(&b, 0, sizeof(b));\n-\tif (!parse_objectish_with_prefix(&b, \"to \"))\n+\tif (!parse_objectish_with_prefix(state, &b, \"to \"))\n \t\tdie(_(\"expected 'to' command, got %s\"), command_buf.buf);\n \te = find_object(&b.oid);\n \tassert(e);\n \tinsert_mark(&marks, next_mark, e);\n }\n \n-static char* make_fast_import_path(const char *path)\n+static char* make_fast_import_path(struct fast_import_state *state, const char *path)\n {\n \tif (!relative_marks_paths || is_absolute_path(path))\n-\t\treturn prefix_filename(global_prefix, path);\n+\t\treturn prefix_filename(state->prefix, path);\n \treturn repo_git_path(the_repository, \"info/fast-import/%s\", path);\n }\n \n-static void option_import_marks(const char *marks,\n+static void option_import_marks(struct fast_import_state *state, const char *marks,\n \t\t\t\t\tint from_stream, int ignore_missing)\n {\n \tif (import_marks_file) {\n@@ -3663,7 +3677,7 @@ static void option_import_marks(const char *marks,\n \t}\n \n \tfree(import_marks_file);\n-\timport_marks_file = make_fast_import_path(marks);\n+\timport_marks_file = make_fast_import_path(state, marks);\n \timport_marks_file_from_stream = from_stream;\n \timport_marks_file_ignore_missing = ignore_missing;\n }\n@@ -3703,13 +3717,13 @@ static void option_active_branches(const char *branches)\n \tmax_active_branches = ulong_arg(\"--active-branches\", branches);\n }\n \n-static void option_export_marks(const char *marks)\n+static void option_export_marks(struct fast_import_state *state, const char *marks)\n {\n \tfree(export_marks_file);\n-\texport_marks_file = make_fast_import_path(marks);\n+\texport_marks_file = make_fast_import_path(state, marks);\n }\n \n-static void option_cat_blob_fd(const char *fd)\n+static void option_cat_blob_fd(struct fast_import_state *state UNUSED, const char *fd)\n {\n \tunsigned long n = ulong_arg(\"--cat-blob-fd\", fd);\n \tif (n > (unsigned long) INT_MAX)\n@@ -3717,16 +3731,16 @@ static void option_cat_blob_fd(const char *fd)\n \tcat_blob_fd = (int) n;\n }\n \n-static void option_export_pack_edges(const char *edges)\n+static void option_export_pack_edges(struct fast_import_state *state, const char *edges)\n {\n-\tchar *fn = prefix_filename(global_prefix, edges);\n+\tchar *fn = prefix_filename(state->prefix, edges);\n \tif (pack_edges)\n \t\tfclose(pack_edges);\n \tpack_edges = xfopen(fn, \"a\");\n \tfree(fn);\n }\n \n-static void option_rewrite_submodules(const char *arg, struct string_list *list)\n+static void option_rewrite_submodules(struct fast_import_state *state, const char *arg, struct string_list *list)\n {\n \tstruct mark_set *ms;\n \tFILE *fp;\n@@ -3738,7 +3752,7 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tf++;\n \tCALLOC_ARRAY(ms, 1);\n \n-\tf = prefix_filename(global_prefix, f);\n+\tf = prefix_filename(state->prefix, f);\n \tfp = fopen(f, \"r\");\n \tif (!fp)\n \t\tdie_errno(_(\"cannot read '%s'\"), f);\n@@ -3794,7 +3808,7 @@ static void option_quiet(void)\n \tquiet = 1;\n }\n \n-static int parse_one_option(const char *option)\n+static int parse_one_option(struct fast_import_state *state, const char *option)\n {\n \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n \t\toption_max_pack_size(option);\n@@ -3805,7 +3819,7 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n \t\toption_active_branches(option);\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n-\t\toption_export_pack_edges(option);\n+\t\toption_export_pack_edges(state, option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n \t\toption_signed_commits(option);\n \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n@@ -3823,34 +3837,34 @@ static int parse_one_option(const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n {\n \tif (from_stream && !allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n \n-static int parse_one_feature(const char *feature, int from_stream)\n+static int parse_one_feature(struct fast_import_state *state, const char *feature, int from_stream)\n {\n \tconst char *arg;\n \n \tif (skip_prefix(feature, \"date-format=\", &arg)) {\n \t\toption_date_format(arg);\n \t} else if (skip_prefix(feature, \"import-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 0);\n+\t\tcheck_unsafe_feature(state, \"import-marks\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 0);\n \t} else if (skip_prefix(feature, \"import-marks-if-exists=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks-if-exists\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 1);\n+\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 1);\n \t} else if (skip_prefix(feature, \"export-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(feature, from_stream);\n-\t\toption_export_marks(arg);\n+\t\tcheck_unsafe_feature(state, feature, from_stream);\n+\t\toption_export_marks(state, arg);\n \t} else if (!strcmp(feature, \"alias\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (skip_prefix(feature, \"rewrite-submodules-to=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_to);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_to);\n \t} else if (skip_prefix(feature, \"rewrite-submodules-from=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_from);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_from);\n \t} else if (!strcmp(feature, \"get-mark\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (!strcmp(feature, \"cat-blob\")) {\n@@ -3872,23 +3886,23 @@ static int parse_one_feature(const char *feature, int from_stream)\n \treturn 1;\n }\n \n-static void parse_feature(const char *feature)\n+static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n-\tif (parse_one_feature(feature, 1))\n+\tif (parse_one_feature(state, feature, 1))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support feature %s.\"), feature);\n }\n \n-static void parse_option(const char *option)\n+static void parse_option(struct fast_import_state *state, const char *option)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n-\tif (parse_one_option(option))\n+\tif (parse_one_option(state, option))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support option: %s\"), option);\n@@ -3924,12 +3938,12 @@ static void git_pack_config(void)\n static const char fast_import_usage[] =\n \"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n \n-static void parse_argv(void)\n+static void parse_argv(struct fast_import_state *state)\n {\n \tunsigned int i;\n \n-\tfor (i = 1; i < global_argc; i++) {\n-\t\tconst char *a = global_argv[i];\n+\tfor (i = 1; i < state->argc; i++) {\n+\t\tconst char *a = state->argv[i];\n \n \t\tif (*a != '-' || !strcmp(a, \"--\"))\n \t\t\tbreak;\n@@ -3937,20 +3951,20 @@ static void parse_argv(void)\n \t\tif (!skip_prefix(a, \"--\", &a))\n \t\t\tdie(_(\"unknown option %s\"), a);\n \n-\t\tif (parse_one_option(a))\n+\t\tif (parse_one_option(state, a))\n \t\t\tcontinue;\n \n-\t\tif (parse_one_feature(a, 0))\n+\t\tif (parse_one_feature(state, a, 0))\n \t\t\tcontinue;\n \n \t\tif (skip_prefix(a, \"cat-blob-fd=\", &a)) {\n-\t\t\toption_cat_blob_fd(a);\n+\t\t\toption_cat_blob_fd(state, a);\n \t\t\tcontinue;\n \t\t}\n \n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n-\tif (i != global_argc)\n+\tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n \tseen_data_command = 1;\n@@ -3964,6 +3978,8 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n+\tstruct fast_import_state state;\n+\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3992,9 +4008,7 @@ int cmd_fast_import(int argc,\n \t\t\tallow_unsafe_features = 1;\n \t}\n \n-\tglobal_argc = argc;\n-\tglobal_argv = argv;\n-\tglobal_prefix = prefix;\n+\tfast_import_state_init(&state, argc, argv, prefix);\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n@@ -4004,34 +4018,34 @@ int cmd_fast_import(int argc,\n \tstart_packfile();\n \tset_die_routine(die_nicely);\n \tset_checkpoint_signal();\n-\twhile (read_next_command() != EOF) {\n+\twhile (read_next_command(&state) != EOF) {\n \t\tconst char *v;\n \t\tif (!strcmp(\"blob\", command_buf.buf))\n-\t\t\tparse_new_blob();\n+\t\t\tparse_new_blob(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"commit \", &v))\n-\t\t\tparse_new_commit(v);\n+\t\t\tparse_new_commit(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"tag \", &v))\n-\t\t\tparse_new_tag(v);\n+\t\t\tparse_new_tag(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"reset \", &v))\n-\t\t\tparse_reset_branch(v);\n+\t\t\tparse_reset_branch(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, NULL);\n+\t\t\tparse_ls(&state, v, NULL);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"get-mark \", &v))\n-\t\t\tparse_get_mark(v);\n+\t\t\tparse_get_mark(&state, v);\n \t\telse if (!strcmp(\"checkpoint\", command_buf.buf))\n-\t\t\tparse_checkpoint();\n+\t\t\tparse_checkpoint(&state);\n \t\telse if (!strcmp(\"done\", command_buf.buf))\n \t\t\tbreak;\n \t\telse if (!strcmp(\"alias\", command_buf.buf))\n-\t\t\tparse_alias();\n+\t\t\tparse_alias(&state);\n \t\telse if (starts_with(command_buf.buf, \"progress \"))\n-\t\t\tparse_progress();\n+\t\t\tparse_progress(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"feature \", &v))\n-\t\t\tparse_feature(v);\n+\t\t\tparse_feature(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"option git \", &v))\n-\t\t\tparse_option(v);\n+\t\t\tparse_option(&state, v);\n \t\telse if (starts_with(command_buf.buf, \"option \"))\n \t\t\t/* ignore non-git options*/;\n \t\telse\n@@ -4043,7 +4057,7 @@ int cmd_fast_import(int argc,\n \n \t/* argv hasn't been parsed yet, do so */\n \tif (!seen_data_command)\n-\t\tparse_argv();\n+\t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n \t\tdie(_(\"stream ends early\"));\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549560","messageId":"20260804100355.1299498-9-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 08/12] fast-import: move command state globals into 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:51Z","receivedAt":"2026-08-04T10:04:31Z","isPatch":true,"body":"A previous commit introduced 'struct fast_import_state' to hold some\ncommand state, and reduce the need for global variables.\n\nLet's continue in the same direction and move two more global variables\nthat describe the command state into it: 'seen_data_command' and\n'allow_unsafe_features'.\n\nAll the sites accessing these variables are already in functions that\nreceive the 'state' parameter (or in cmd_fast_import() which owns the\nstruct), so no additional threading is needed.\n\nAs 'state->allow_unsafe_features' is now dereferenced in\ncheck_unsafe_feature(), its 'state' parameter is no longer unused, so\nthe UNUSED marker is removed.\n\nThe fast_import_state_init() call is moved up before the early\ncommand-line scan for '--allow-unsafe-features', so that this option\ncan be recorded directly into the struct without being clobbered by\nthe memset() in fast_import_state_init().\n\nThis is a mechanical refactoring with no intended behavior change.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 24 ++++++++++++------------\n 1 file changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex cb0c856e41..926afde954 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -257,9 +257,7 @@ static struct recent_command *rc_free;\n static unsigned int cmd_save = 100;\n static uintmax_t next_mark;\n static struct strbuf new_data = STRBUF_INIT;\n-static int seen_data_command;\n static int require_explicit_termination;\n-static int allow_unsafe_features;\n \n /* Signal handling */\n static volatile sig_atomic_t checkpoint_requested;\n@@ -277,6 +275,8 @@ struct fast_import_state {\n \tint argc;\n \tconst char **argv;\n \tconst char *prefix;\n+\tint seen_data_command;\n+\tint allow_unsafe_features;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n@@ -1877,7 +1877,7 @@ static int read_next_command(struct fast_import_state *state)\n \t\t\tif (stdin_eof)\n \t\t\t\treturn EOF;\n \n-\t\t\tif (!seen_data_command\n+\t\t\tif (!state->seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n \t\t\t\tparse_argv(state);\n@@ -3837,9 +3837,9 @@ static int parse_one_option(struct fast_import_state *state, const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state, const char *feature, int from_stream)\n {\n-\tif (from_stream && !allow_unsafe_features)\n+\tif (from_stream && !state->allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n@@ -3888,7 +3888,7 @@ static int parse_one_feature(struct fast_import_state *state, const char *featur\n \n static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n \tif (parse_one_feature(state, feature, 1))\n@@ -3899,7 +3899,7 @@ static void parse_feature(struct fast_import_state *state, const char *feature)\n \n static void parse_option(struct fast_import_state *state, const char *option)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n \tif (parse_one_option(state, option))\n@@ -3967,7 +3967,7 @@ static void parse_argv(struct fast_import_state *state)\n \tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n-\tseen_data_command = 1;\n+\tstate->seen_data_command = 1;\n \tif (import_marks_file)\n \t\tread_marks();\n \tbuild_mark_map(&sub_marks_from, &sub_marks_to);\n@@ -3982,6 +3982,8 @@ int cmd_fast_import(int argc,\n \n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n+\tfast_import_state_init(&state, argc, argv, prefix);\n+\n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\n \n@@ -4005,11 +4007,9 @@ int cmd_fast_import(int argc,\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n \t\tif (!strcmp(arg, \"--allow-unsafe-features\"))\n-\t\t\tallow_unsafe_features = 1;\n+\t\t\tstate.allow_unsafe_features = 1;\n \t}\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n-\n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n@@ -4056,7 +4056,7 @@ int cmd_fast_import(int argc,\n \t}\n \n \t/* argv hasn't been parsed yet, do so */\n-\tif (!seen_data_command)\n+\tif (!state.seen_data_command)\n \t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549561","messageId":"20260804100355.1299498-10-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 09/12] fast-import: use struct option for usage string","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:52Z","receivedAt":"2026-08-04T10:04:32Z","isPatch":true,"body":"Currently `git fast-import -h` shows the following on a single line:\n\n  usage : git fast-import [--date-format=<f>] [--max-pack-size=<n>] \\\n                          [--big-file-threshold=<n>] [--depth=<n>] \\\n                          [--active-branches=<n>] \\\n                          [--export-marks=<marks.file>]\n\nThis output has a number of issues like:\n\n  - It's missing a lot of options.\n  - It's not consistent with the SYNOPSIS section of the doc.\n  - With `--help-all` instead of `-h` additional hidden options should\n    be shown, but that's not the case.\n  - It's not standard style anymore.\n  - Most other Git commands show additional lines for most of the\n    options they support.\n\nAlso while most commands use the parse-options API to handle their\noptions, \"builtin/fast-import.c\" still doesn't use it.\n\nLet's improve on that by using the parse-options API to display the\noptions when `-h` and `--help-all` are used.\n\nWhile at it, let's make the SYNOPSIS section of\n\"Documentation/git-fast-import.adoc\" consistent with the new usage\nstring.\n\nThis deliberately leaves it to future work to also use the\nparse-options API to actually parse the options.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n Documentation/git-fast-import.adoc |  2 +-\n builtin/fast-import.c              | 83 +++++++++++++++++++++++++++---\n t/t0450/adoc-help-mismatches       |  1 -\n 3 files changed, 78 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex d68bc52b7e..7c5900e048 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -9,7 +9,7 @@ git-fast-import - Backend for fast Git data importers\n SYNOPSIS\n --------\n [verse]\n-frontend | 'git fast-import' [<options>]\n+'git fast-import' [<options>]\n \n DESCRIPTION\n -----------\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 926afde954..bf72adf62b 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -30,6 +30,7 @@\n #include \"khash.h\"\n #include \"date.h\"\n #include \"gpg-interface.h\"\n+#include \"parse-options.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -277,16 +278,18 @@ struct fast_import_state {\n \tconst char *prefix;\n \tint seen_data_command;\n \tint allow_unsafe_features;\n+\tstruct option *option;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n \t\t\t\t   int argc, const char **argv,\n-\t\t\t\t   const char *prefix)\n+\t\t\t\t   const char *prefix, struct option *option)\n {\n \tmemset(state, 0, sizeof(*state));\n \tstate->argc = argc;\n \tstate->argv = argv;\n \tstate->prefix = prefix;\n+\tstate->option = option;\n }\n \n static void parse_argv(struct fast_import_state *state);\n@@ -3935,8 +3938,10 @@ static void git_pack_config(void)\n \trepo_config(the_repository, git_default_config, NULL);\n }\n \n-static const char fast_import_usage[] =\n-\"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n+static const char *const fast_import_usage[] = {\n+\tN_(\"git fast-import [<options>]\"),\n+\tNULL\n+};\n \n static void parse_argv(struct fast_import_state *state)\n {\n@@ -3965,7 +3970,7 @@ static void parse_argv(struct fast_import_state *state)\n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n \tif (i != state->argc)\n-\t\tusage(fast_import_usage);\n+\t\tusage_with_options(fast_import_usage, state->option);\n \n \tstate->seen_data_command = 1;\n \tif (import_marks_file)\n@@ -3980,9 +3985,75 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\tshow_usage_if_asked(argc, argv, fast_import_usage);\n+\tunsigned long pack_size_limit, big_file_threshold;\n+\tchar *edges, *signed_commits, *signed_tags, *date_format;\n+\tchar *import_marks_if_exists, *submodules_from, *submodules_to;\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n+\t/*\n+\t * NEEDSWORK: For now this is used only to render\n+\t * `-h`/`--help-all` usage messages. The actual parsing is\n+\t * done by parse_one_option()/parse_one_feature().\n+\t */\n+\tstruct option fast_import_options[] = {\n+\t\tOPT_GROUP(N_(\"Common\")),\n+\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n+\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"stats\", &show_stats,\n+\t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n+\t\t\t   PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"quiet\", &quiet,\n+\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"force\", &force_update,\n+\t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"done\", &require_explicit_termination,\n+\t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n+\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n+\t\t\t     N_(\"maximum size of each output pack file\")),\n+\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n+\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n+\t\tOPT_UNSIGNED(0, \"depth\", &max_depth,\n+\t\t\t     N_(\"maximum delta depth\")),\n+\t\tOPT_UNSIGNED(0, \"active-branches\", &max_active_branches,\n+\t\t\t     N_(\"maximum number of branches to maintain active\")),\n+\t\tOPT_GROUP(N_(\"Marks\")),\n+\t\tOPT_STRING_F(0, \"import-marks\", &import_marks_file, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-marks\", &export_marks_file, N_(\"file\"),\n+\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL(0, \"relative-marks\", &relative_marks_paths,\n+\t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n+\t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-to\", &submodules_to, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_GROUP(N_(\"Signing\")),\n+\t\tOPT_STRING_F(0, \"signed-commits\", &signed_commits, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed commits\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"signed-tags\", &signed_tags, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed tags\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n+\t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &state.allow_unsafe_features,\n+\t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n+\t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n+\t\t\t     N_(\"dump edge commits to <file>\"),\n+\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob_fd,\n+\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n+\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_END()\n+\t};\n+\n+\tshow_usage_with_options_if_asked(argc, argv, fast_import_usage, fast_import_options);\n+\n+\tfast_import_state_init(&state, argc, argv, prefix, fast_import_options);\n \n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\ndiff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches\nindex c4a55ff4e3..baf3b1d809 100644\n--- a/t/t0450/adoc-help-mismatches\n+++ b/t/t0450/adoc-help-mismatches\n@@ -12,7 +12,6 @@ column\n credential\n credential-cache\n credential-store\n-fast-import\n fetch-pack\n fmt-merge-msg\n format-patch\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549562","messageId":"20260804100355.1299498-11-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 10/12] fast-import: use callbacks to parse some options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:53Z","receivedAt":"2026-08-04T10:04:33Z","isPatch":true,"body":"A previous commit started using the parse-option API to generate proper\n`git fast-import -h` and `git fast-import --help-all` output.\n\nLet's prepare for when we can use that API to also parse the options by\nusing OPT_CALLBACK for some options that require special processing of\ntheir arguments.\n\nA following commit will actually parse the options using these\ncallbacks.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 208 ++++++++++++++++++++++++++++++++++--------\n 1 file changed, 168 insertions(+), 40 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex bf72adf62b..f3c46fb567 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3978,6 +3978,126 @@ static void parse_argv(struct fast_import_state *state)\n \tbuild_mark_map(&sub_marks_from, &sub_marks_to);\n }\n \n+static int option_parse_date_format(const struct option *opt UNUSED,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_date_format(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_export_pack_edges(const struct option *opt,\n+\t\t\t\t\t  const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_export_pack_edges(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_max_pack_size(const struct option *opt UNUSED,\n+\t\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_max_pack_size(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_big_file_threshold(const struct option *opt UNUSED,\n+\t\t\t\t\t   const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_big_file_threshold(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_signed_commits(const struct option *opt UNUSED,\n+\t\t\t\t       const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_signed_commits(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_signed_tags(const struct option *opt UNUSED,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_signed_tags(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_rewrite_submodules_from(const struct option *opt,\n+\t\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_rewrite_submodules(opt->value, arg, &sub_marks_from);\n+\treturn 0;\n+}\n+\n+static int option_parse_rewrite_submodules_to(const struct option *opt,\n+\t\t\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_rewrite_submodules(opt->value, arg, &sub_marks_to);\n+\treturn 0;\n+}\n+\n+static int option_parse_cat_blob_fd(const struct option *opt,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_cat_blob_fd(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_import_marks(const struct option *opt,\n+\t\t\t\t     const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_import_marks(opt->value, arg, 0, 0);\n+\treturn 0;\n+}\n+\n+static int option_parse_import_marks_if_exists(const struct option *opt,\n+\t\t\t\t\t       const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_import_marks(opt->value, arg, 0, 1);\n+\treturn 0;\n+}\n+\n+static int option_parse_export_marks(const struct option *opt,\n+\t\t\t\t     const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_export_marks(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_depth(const struct option *opt UNUSED,\n+\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_depth(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_active_branches(const struct option *opt UNUSED,\n+\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_active_branches(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_quiet(const struct option *opt UNUSED,\n+\t\t\t      const char *arg UNUSED, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_quiet();\n+\treturn 0;\n+}\n+\n int cmd_fast_import(int argc,\n \t\t    const char **argv,\n \t\t    const char *prefix,\n@@ -3985,10 +4105,6 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\tunsigned long pack_size_limit, big_file_threshold;\n-\tchar *edges, *signed_commits, *signed_tags, *date_format;\n-\tchar *import_marks_if_exists, *submodules_from, *submodules_to;\n-\n \t/*\n \t * NEEDSWORK: For now this is used only to render\n \t * `-h`/`--help-all` usage messages. The actual parsing is\n@@ -3996,58 +4112,70 @@ int cmd_fast_import(int argc,\n \t */\n \tstruct option fast_import_options[] = {\n \t\tOPT_GROUP(N_(\"Common\")),\n-\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n-\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"date-format\", NULL, N_(\"fmt\"),\n+\t\t\t       N_(\"format of the commit/tag dates\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_date_format),\n \t\tOPT_BOOL_F(0, \"stats\", &show_stats,\n \t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n \t\t\t   PARSE_OPT_NONEG),\n-\t\tOPT_BOOL_F(0, \"quiet\", &quiet,\n-\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"quiet\", NULL, NULL,\n+\t\t\t       N_(\"disable the output shown by --stats\"),\n+\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_quiet),\n \t\tOPT_BOOL_F(0, \"force\", &force_update,\n \t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n \t\tOPT_BOOL_F(0, \"done\", &require_explicit_termination,\n \t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n-\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n-\t\t\t     N_(\"maximum size of each output pack file\")),\n-\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n-\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n-\t\tOPT_UNSIGNED(0, \"depth\", &max_depth,\n-\t\t\t     N_(\"maximum delta depth\")),\n-\t\tOPT_UNSIGNED(0, \"active-branches\", &max_active_branches,\n-\t\t\t     N_(\"maximum number of branches to maintain active\")),\n+\t\tOPT_CALLBACK_F(0, \"max-pack-size\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum size of each output pack file\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_max_pack_size),\n+\t\tOPT_CALLBACK_F(0, \"big-file-threshold\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum size of a blob that will be deltified\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_big_file_threshold),\n+\t\tOPT_CALLBACK_F(0, \"depth\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum delta depth\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_depth),\n+\t\tOPT_CALLBACK_F(0, \"active-branches\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum number of branches to maintain active\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_active_branches),\n \t\tOPT_GROUP(N_(\"Marks\")),\n-\t\tOPT_STRING_F(0, \"import-marks\", &import_marks_file, N_(\"file\"),\n-\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n-\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"export-marks\", &export_marks_file, N_(\"file\"),\n-\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"import-marks\", &state, N_(\"file\"),\n+\t\t\t       N_(\"import marks from <file>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_import_marks),\n+\t\tOPT_CALLBACK_F(0, \"import-marks-if-exists\", &state, N_(\"file\"),\n+\t\t\t       N_(\"import marks from <file> if it exists\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_import_marks_if_exists),\n+\t\tOPT_CALLBACK_F(0, \"export-marks\", &state, N_(\"file\"),\n+\t\t\t       N_(\"dump marks to <file>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_export_marks),\n \t\tOPT_BOOL(0, \"relative-marks\", &relative_marks_paths,\n \t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n \t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n-\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n-\t\t\t     N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n-\t\t\t     PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"rewrite-submodules-to\", &submodules_to, N_(\"name:filename\"),\n-\t\t\t     N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n-\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"rewrite-submodules-from\", &state, N_(\"name:filename\"),\n+\t\t\t       N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_rewrite_submodules_from),\n+\t\tOPT_CALLBACK_F(0, \"rewrite-submodules-to\", &state, N_(\"name:filename\"),\n+\t\t\t       N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_rewrite_submodules_to),\n \t\tOPT_GROUP(N_(\"Signing\")),\n-\t\tOPT_STRING_F(0, \"signed-commits\", &signed_commits, N_(\"mode\"),\n-\t\t\t     N_(\"how to handle signed commits\"),\n-\t\t\t     PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"signed-tags\", &signed_tags, N_(\"mode\"),\n-\t\t\t     N_(\"how to handle signed tags\"),\n-\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"signed-commits\", NULL, N_(\"mode\"),\n+\t\t\t       N_(\"how to handle signed commits\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_signed_commits),\n+\t\tOPT_CALLBACK_F(0, \"signed-tags\", NULL, N_(\"mode\"),\n+\t\t\t       N_(\"how to handle signed tags\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_signed_tags),\n \t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n \t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &state.allow_unsafe_features,\n \t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n \t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n-\t\t\t     N_(\"dump edge commits to <file>\"),\n-\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n-\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob_fd,\n-\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n-\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"export-pack-edges\", &state, N_(\"file\"),\n+\t\t\t       N_(\"dump edge commits to <file>\"),\n+\t\t\t       PARSE_OPT_HIDDEN | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_export_pack_edges),\n+\t\tOPT_CALLBACK_F(0, \"cat-blob-fd\", &state, N_(\"fd\"),\n+\t\t\t       N_(\"write some responses to <fd> instead of stdout\"),\n+\t\t\t       PARSE_OPT_HIDDEN | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_cat_blob_fd),\n \t\tOPT_END()\n \t};\n \n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549563","messageId":"20260804100355.1299498-12-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 11/12] fast-import: use parse_options() for command line options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:54Z","receivedAt":"2026-08-04T10:04:34Z","isPatch":true,"body":"Previous commits have started to use the parse-options API to display\noutput from `git fast-import -h` and `git fast-import --help-all` and\nto prepare for parsing the command line options using this API.\n\nLet's now actually use the API to parse command line options.\n\nThis brings a number of changes that are mostly beneficial:\n\n  - The `--alias`, `--get-mark`, `--cat-blob`, `--ls` and `--notes`\n    options are no longer accepted on the command line. They were\n    previously accepted as no-ops because parse_argv() fell through to\n    parse_one_feature(). They are not documented in the OPTIONS section\n    and are only meaningful as in-stream feature assertions, so\n    accepting them on the command line was an accident of code sharing\n    dating back to 9c8398f0c9 (fast-import: add option command,\n    2009-12-04).\n\n  - Abbreviated options like `--dep=5` now work since parse_options()\n    allows unambiguous prefixes.\n\n  - As `--cat-blob` is an abbreviation of `--cat-blob-fd`, using the\n    former on the command line will fail with \"option `cat-blob-fd'\n    requires a value\" unlike the other four options that are not\n    accepted anymore on the command line (see above).\n\n  - The error messages for some options might differ a bit.\n\n  - The code is shorter and more standard.\n\nNote that parse_one_feature() is now always called with its\n`from_stream` argument set to 1, but the code simplifications that\ncan be made are left for a following clean-up commit.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c  | 33 ++++-----------------------------\n t/t9300-fast-import.sh |  7 +++++++\n 2 files changed, 11 insertions(+), 29 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex f3c46fb567..0df7a31014 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3945,31 +3945,11 @@ static const char *const fast_import_usage[] = {\n \n static void parse_argv(struct fast_import_state *state)\n {\n-\tunsigned int i;\n-\n-\tfor (i = 1; i < state->argc; i++) {\n-\t\tconst char *a = state->argv[i];\n-\n-\t\tif (*a != '-' || !strcmp(a, \"--\"))\n-\t\t\tbreak;\n+\tint argc = parse_options(state->argc, state->argv, state->prefix,\n+\t\t\t\t state->option, fast_import_usage,\n+\t\t\t\t PARSE_OPT_KEEP_ARGV0);\n \n-\t\tif (!skip_prefix(a, \"--\", &a))\n-\t\t\tdie(_(\"unknown option %s\"), a);\n-\n-\t\tif (parse_one_option(state, a))\n-\t\t\tcontinue;\n-\n-\t\tif (parse_one_feature(state, a, 0))\n-\t\t\tcontinue;\n-\n-\t\tif (skip_prefix(a, \"cat-blob-fd=\", &a)) {\n-\t\t\toption_cat_blob_fd(state, a);\n-\t\t\tcontinue;\n-\t\t}\n-\n-\t\tdie(_(\"unknown option --%s\"), a);\n-\t}\n-\tif (i != state->argc)\n+\tif (argc > 1)\n \t\tusage_with_options(fast_import_usage, state->option);\n \n \tstate->seen_data_command = 1;\n@@ -4105,11 +4085,6 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\t/*\n-\t * NEEDSWORK: For now this is used only to render\n-\t * `-h`/`--help-all` usage messages. The actual parsing is\n-\t * done by parse_one_option()/parse_one_feature().\n-\t */\n \tstruct option fast_import_options[] = {\n \t\tOPT_GROUP(N_(\"Common\")),\n \t\tOPT_CALLBACK_F(0, \"date-format\", NULL, N_(\"fmt\"),\ndiff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh\nindex fe6c2617ac..d9de2ef0d8 100755\n--- a/t/t9300-fast-import.sh\n+++ b/t/t9300-fast-import.sh\n@@ -2827,6 +2827,13 @@ test_expect_success 'R: unknown commandline options are rejected' '\\\n \ttest_must_fail git fast-import --non-existing-option < /dev/null\n '\n \n+test_expect_success 'R: feature-only names are rejected on the command line' '\n+\tfor opt in --alias --get-mark --ls --notes\n+\tdo\n+\t\ttest_must_fail git fast-import \"$opt\" </dev/null || return 1\n+\tdone\n+'\n+\n test_expect_success 'R: die on invalid option argument' '\n \techo \"option git active-branches=-5\" |\n \ttest_must_fail git fast-import &&\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"549564","messageId":"20260804100355.1299498-13-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v2 12/12] fast-import: remove useless from_stream argument","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-04T10:03:55Z","receivedAt":"2026-08-04T10:04:35Z","isPatch":true,"body":"Now that a previous commit has removed a call to parse_one_feature()\nfrom parse_argv(), the former is always called with its `from_stream`\nargument set to 1.\n\nLet's take advantage of that to simplify and cleanup the code a bit.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 18 +++++++++---------\n 1 file changed, 9 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 0df7a31014..9d827f2224 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3840,27 +3840,27 @@ static int parse_one_option(struct fast_import_state *state, const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(struct fast_import_state *state, const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state, const char *feature)\n {\n-\tif (from_stream && !state->allow_unsafe_features)\n+\tif (!state->allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n \n-static int parse_one_feature(struct fast_import_state *state, const char *feature, int from_stream)\n+static int parse_one_feature(struct fast_import_state *state, const char *feature)\n {\n \tconst char *arg;\n \n \tif (skip_prefix(feature, \"date-format=\", &arg)) {\n \t\toption_date_format(arg);\n \t} else if (skip_prefix(feature, \"import-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, \"import-marks\", from_stream);\n-\t\toption_import_marks(state, arg, from_stream, 0);\n+\t\tcheck_unsafe_feature(state, \"import-marks\");\n+\t\toption_import_marks(state, arg, 1, 0);\n \t} else if (skip_prefix(feature, \"import-marks-if-exists=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\", from_stream);\n-\t\toption_import_marks(state, arg, from_stream, 1);\n+\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\");\n+\t\toption_import_marks(state, arg, 1, 1);\n \t} else if (skip_prefix(feature, \"export-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, feature, from_stream);\n+\t\tcheck_unsafe_feature(state, feature);\n \t\toption_export_marks(state, arg);\n \t} else if (!strcmp(feature, \"alias\")) {\n \t\t; /* Don't die - this feature is supported */\n@@ -3894,7 +3894,7 @@ static void parse_feature(struct fast_import_state *state, const char *feature)\n \tif (state->seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n-\tif (parse_one_feature(state, feature, 1))\n+\tif (parse_one_feature(state, feature))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support feature %s.\"), feature);\n-- \n2.55.0.492.g44bba30fd7.dirty\n\n"},{"id":"550085","messageId":"CABPp-BGfeF1t+siEUuYgTtDG5LtfL5iskSHferbGwFj8axA+tA@mail.gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-3-christian.couder@gmail.com","subject":"Re: [PATCH v2 02/12] api-parse-options.adoc: document per-option flags","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-08T07:25:22Z","receivedAt":"2026-08-08T07:25:35Z","isPatch":true,"body":"On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> The \"Flags\" section in \"Documentation/technical/api-parse-options.adoc\"\n> documents the flags that can be passed to parse_options() itself. It\n> does not, however, document the flags that can be set on individual\n> options through the `flags` member of `struct option` (and through the\n> `OPT_*_F()` macro variants).\n>\n> These per-option flags are used throughout the codebase (for example\n> `PARSE_OPT_HIDDEN` is used to hide an option from `-h` while still\n> showing it with `--help-all`), but a reader currently has to dig into\n> \"parse-options.h\" to find them.\n>\n> To remediate that, let's add an \"Option flags\" subsection to the\n> \"Data Structure\" section, just before the list of option macros.\n>\n> Let's also make it explicit that these are distinct from the\n> parse_options() flags described earlier, and let's describe the `-h`\n> versus `--help-all` behavior for `PARSE_OPT_HIDDEN`.\n>\n> Signed-off-by: Christian Couder <christian.couder@gmail.com>\n> ---\n>  .../technical/api-parse-options.adoc          | 61 +++++++++++++++++++\n>  1 file changed, 61 insertions(+)\n>\n> diff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\n> index 880eb94642..fb4580e755 100644\n> --- a/Documentation/technical/api-parse-options.adoc\n> +++ b/Documentation/technical/api-parse-options.adoc\n> @@ -150,6 +150,67 @@ Data Structure\n>\n>  The main data structure is an array of the `option` struct,\n>  say `static struct option builtin_add_options[]`.\n> +\n> +Option flags\n> +~~~~~~~~~~~~\n> +\n> +Each option can carry flags in the `flags` field of its `option`\n> +struct. These are per-option flags and are distinct from the\n> +`parse_options()` flags described above; they are usually set through\n> +the `OPT_*_F()` macro variants (see below) rather than by hand. They\n> +are the bitwise-or of:\n> +\n> +`PARSE_OPT_OPTARG`::\n> +       The option's argument is optional, i.e. both `--option` and\n> +       `--option=<value>` are accepted.\n> +\n> +`PARSE_OPT_NOARG`::\n> +       The option takes no argument at all. Using `--option=<value>`\n> +       is rejected.\n> +\n> +`PARSE_OPT_NONEG`::\n> +       Disable the automatically generated negated `--no-option`\n> +       form.\n> +\n> +`PARSE_OPT_HIDDEN`::\n> +       Hide the option: it is omitted from the usage shown by\n> +       `git <cmd> -h`, but is still shown by `git <cmd> --help-all`.\n> +       The option is parsed as usual either way. This is meant for\n> +       deprecated, advanced or otherwise uncommon options.\n> +\n> +`PARSE_OPT_LASTARG_DEFAULT`::\n> +       Use the default value (`defval`) when the option is used\n> +       without an argument, even for an option that normally requires\n> +       one. Only the last argument on the command line takes effect.\n\nIs this accurate?  Sufficiently precise?  parse-options.h says\n\n *   PARSE_OPT_LASTARG_DEFAULT: says that this option will take the default\n *                value if no argument is given when the option\n *                is last on the command line. If the option is\n *                not last it will require an argument.\n *                Should not be used with PARSE_OPT_OPTARG.\n\nIf you want to reword that, maybe something like:\n\n        The no-argument form is only accepted when the option is the\n        last token on the command line; used earlier, it still\n        requires an argument. Should not be combined with\n        `PARSE_OPT_OPTARG`.\n\n?\n\n> +\n> +`PARSE_OPT_NODASH`::\n> +       The option is a single character without a leading dash, such\n> +       as the `+` used by some commands.\n> +\n> +`PARSE_OPT_LITERAL_ARGHELP`::\n> +       Use the argument help string (`argh`) verbatim in the usage\n> +       output instead of surrounding it with `<>` or `[]`. Useful when\n> +       `argh` already contains a hand-formatted description.\n> +\n> +`PARSE_OPT_FROM_ALIAS`::\n> +       Internal flag, set on options that were expanded from a\n> +       configured alias. It should not be set by callers.\n> +\n> +`PARSE_OPT_NOCOMPLETE`::\n> +       Do not offer this option for completion.\n> +\n> +`PARSE_OPT_COMP_ARG`::\n> +       The option's argument, rather than the option itself, is what\n> +       should be completed.\n> +\n> +`PARSE_OPT_CMDMODE`::\n> +       The option is one of several mutually exclusive \"command mode\"\n> +       options that share the same variable. Using more than one of\n> +       them at once is rejected.\n\nThanks for adding this table; looks helpful.\n"},{"id":"550086","messageId":"CABPp-BHoxLkYJmoJ1N5owJ5-S+yr-4JkuectxSVG8oa6PESkWA@mail.gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-8-christian.couder@gmail.com","subject":"Re: [PATCH v2 07/12] fast-import: introduce 'struct fast_import_state'","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-08T07:25:33Z","receivedAt":"2026-08-08T07:25:45Z","isPatch":true,"body":"On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n\n> -static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n> +static void note_change_n(struct fast_import_state *state, const char *p, struct branch *b, unsigned char *old_fanout)\n\nA really minor comment, but you've taken several lines (some of which\nwere already too long) and made them much too long.  This wasn't the\nfirst or the last, but at 118 columns it was particularly far from the\n80 characters per line guideline.  Could we change to\n\nstatic void note_change_n(struct fast_import_state *state,\n              const char *p,\n              struct branch *b,\n              unsigned char *old_fanout)\n\n?\n\nThe actual substance of the patch looks good.\n"},{"id":"550087","messageId":"CABPp-BFfF+Vd6RY3pG=FVUH_93YZULfhcXdWCv6zcRSABfGBQQ@mail.gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-12-christian.couder@gmail.com","subject":"Re: [PATCH v2 11/12] fast-import: use parse_options() for command line options","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-08T07:25:37Z","receivedAt":"2026-08-08T07:25:50Z","isPatch":true,"body":"On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> Previous commits have started to use the parse-options API to display\n> output from `git fast-import -h` and `git fast-import --help-all` and\n> to prepare for parsing the command line options using this API.\n>\n> Let's now actually use the API to parse command line options.\n>\n> This brings a number of changes that are mostly beneficial:\n>\n>   - The `--alias`, `--get-mark`, `--cat-blob`, `--ls` and `--notes`\n>     options are no longer accepted on the command line. They were\n>     previously accepted as no-ops because parse_argv() fell through to\n>     parse_one_feature(). They are not documented in the OPTIONS section\n>     and are only meaningful as in-stream feature assertions, so\n>     accepting them on the command line was an accident of code sharing\n>     dating back to 9c8398f0c9 (fast-import: add option command,\n>     2009-12-04).\n>\n>   - Abbreviated options like `--dep=5` now work since parse_options()\n>     allows unambiguous prefixes.\n>\n>   - As `--cat-blob` is an abbreviation of `--cat-blob-fd`, using the\n>     former on the command line will fail with \"option `cat-blob-fd'\n>     requires a value\" unlike the other four options that are not\n>     accepted anymore on the command line (see above).\n>\n>   - The error messages for some options might differ a bit.\n>\n>   - The code is shorter and more standard.\n\nI think the list might be missing three behavioral changes:\n\n1) A bare \"--\" (or a trailing \"--\") is now accepted and the command\nreads the stream normally, whereas the base treated it as a usage\nerror:\n\n        printf '' | git fast-import --   # before: 129 (usage), after: 0\n\nThe old parse_argv() broke on \"--\" and then did \"if (i != state->argc)\nusage_with_options(...)\"; parse_options() instead consumes \"--\" as the\nend-of-options marker and returns just argv0.  Harmless /\nconventional, just unlisted.\n\n2) Value-taking options now also accept the space-separated \"--opt\nvalue\" form (e.g. \"--depth 5\", \"--max-pack-size 1m\", \"--date-format\nraw\"), not just \"--opt=value\".   Also expected parse_options()\nbehavior and a nice improvement.\n\n3) The handling of \"--allow-unsafe-features\" has changed and might\ntrip users up.\n\nBecause the \"feature\" lines at the top of the stream are processed\nbefore parse_argv() runs, cmd_fast_import() does an early scan just to\nlearn whether unsafe features are permitted:\n\n        for (int i = 1; i < argc; i++) {\n                const char *arg = argv[i];\n                if (*arg != '-' || !strcmp(arg, \"--\"))\n                        break;\n                if (!strcmp(arg, \"--allow-unsafe-features\"))\n                        state.allow_unsafe_features = 1;\n        }\n\nThat scan (a) matches only the exact spelling and (b) stops at the\nfirst token not starting with '-'.  In the base that was fine, because\nthe old parse_argv loop was equally strict (exact spelling,\n\"--opt=value\" only).\nBut now that parse_options() also accepts unambiguous abbreviations\nand space-separated values, the two passes disagree.  With an unsafe\nfeature line in the stream:\n\n    # (A) old-style spelling, still fine:\n    printf 'feature import-marks-if-exists=/nope\\n' |\n      git fast-import --depth=5 --allow-unsafe-features\n    # -> exit 0\n\n    # (B) space-separated value, newly accepted by parse_options():\n    printf 'feature import-marks-if-exists=/nope\\n' |\n      git fast-import --depth 5 --allow-unsafe-features\n    # -> fatal: feature 'import-marks-if-exists' forbidden ... (128)\n\n    # (C) abbreviation, newly accepted by parse_options():\n    printf 'feature import-marks-if-exists=/nope\\n' |\n      git fast-import --allow-unsafe\n    # -> fatal: feature 'import-marks-if-exists' forbidden ... (128)\n\nIn (B) the early scan breaks on the bare \"5\" (it can't tell \"5\" is\n--depth's argument) and never reaches --allow-unsafe-features; in (C)\nthe abbreviation isn't recognized by the strcmp().  Yet in both cases\nparse_options() itself accepts the option (\"git fast-import\n--allow-unsafe\" alone exits 0), so it's only the in-stream feature\nthat gets rejected.\n\nThis errs on the safe side (it refuses an unsafe feature rather than\nallowing one), and it's a minor inconsistency, but it might surprise\nusers.  At a minimum, it should probably be documented as a\nshortcoming or TODO or something.  One possible solution is a\ndedicated parse_options() pass for just --allow-unsafe-features;\nanother might be just requiring --allow-unsafe-features to be the\n*first* argument.  Thoughts?\n\n\nAnyway, other than the above list of three additional behavioral\nchanges, the rest of the patch looks right.  Thanks for tackling\nmodernizing this command.\n"},{"id":"550088","messageId":"CABPp-BE_gVtCF+Y0AAyXSXnJ2hUK0pJiWqEhkD8kVc4S8-y7kQ@mail.gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"Re: [PATCH v2 00/12] fast-import: standardize usage string and SYNOPSIS","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-08T07:26:56Z","receivedAt":"2026-08-08T07:27:10Z","isPatch":true,"body":"On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> The goal of this series is to improve on `git fast-import`'s usage\n> string as it is obsolete in many ways.\n>\n> As it appeared that a good way to reach that goal was to make\n> `git fast-import` use the parse-options API, this series also achieves\n> this secondary goal.\n>\n> Along the way it modernizes \"builtin/fast-import.c\" mostly by using\n> `struct option`, by starting to remove global variables and libify\n> that command, and by introducing a new `OPT_HIDDEN_GROUP` macro.\n>\n> There are still many global variables left, so it's left to future\n> work to finish on that direction.\n>\n> Anyway the usage string is standardized and consistent with the\n> SYNOPSIS in the docs, so that the command can be removed from\n> \"t/t0450/adoc-help-mismatches\".\n>\n> Using the parse-options API also enabled some code standardization and\n> simplification.\n\nThanks for doing the cleanup.  I read through the series and I'm\npretty happy with it; I only found a few minor things to comment on in\nthree of the patches.\n"},{"id":"550246","messageId":"CAP8UFD0usk3kMW-qztB0jokexBnOGtmjH8Se3a2k6nkCP_qdsQ@mail.gmail.com","threadId":"66022","inReplyTo":"CABPp-BGfeF1t+siEUuYgTtDG5LtfL5iskSHferbGwFj8axA+tA@mail.gmail.com","subject":"Re: [PATCH v2 02/12] api-parse-options.adoc: document per-option flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:30:18Z","receivedAt":"2026-08-11T08:30:31Z","isPatch":true,"body":"On Sat, Aug 8, 2026 at 9:25 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n\n> > +`PARSE_OPT_LASTARG_DEFAULT`::\n> > +       Use the default value (`defval`) when the option is used\n> > +       without an argument, even for an option that normally requires\n> > +       one. Only the last argument on the command line takes effect.\n>\n> Is this accurate?  Sufficiently precise?  parse-options.h says\n>\n>  *   PARSE_OPT_LASTARG_DEFAULT: says that this option will take the default\n>  *                value if no argument is given when the option\n>  *                is last on the command line. If the option is\n>  *                not last it will require an argument.\n>  *                Should not be used with PARSE_OPT_OPTARG.\n>\n> If you want to reword that, maybe something like:\n>\n>         The no-argument form is only accepted when the option is the\n>         last token on the command line; used earlier, it still\n>         requires an argument. Should not be combined with\n>         `PARSE_OPT_OPTARG`.\n>\n> ?\n\nYeah, in the v3 I will send soon, I have reworded it like you suggested.\n\nThanks.\n"},{"id":"550247","messageId":"CAP8UFD1V1LA6E8BhuZwn2WaAiidHgZVUkRA4LO_8pMUrsWFu1Q@mail.gmail.com","threadId":"66022","inReplyTo":"CABPp-BHoxLkYJmoJ1N5owJ5-S+yr-4JkuectxSVG8oa6PESkWA@mail.gmail.com","subject":"Re: [PATCH v2 07/12] fast-import: introduce 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:30:33Z","receivedAt":"2026-08-11T08:30:45Z","isPatch":true,"body":"On Sat, Aug 8, 2026 at 9:25 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n> >\n>\n> > -static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n> > +static void note_change_n(struct fast_import_state *state, const char *p, struct branch *b, unsigned char *old_fanout)\n>\n> A really minor comment, but you've taken several lines (some of which\n> were already too long) and made them much too long.  This wasn't the\n> first or the last, but at 118 columns it was particularly far from the\n> 80 characters per line guideline.  Could we change to\n>\n> static void note_change_n(struct fast_import_state *state,\n>               const char *p,\n>               struct branch *b,\n>               unsigned char *old_fanout)\n>\n> ?\n\nYes, in v3 I have changed such lines that are modified by the patch to\nwhat you suggest.\n\n> The actual substance of the patch looks good.\n\nThanks.\n"},{"id":"550248","messageId":"CAP8UFD01-NW21CQ2LfMZ1shKLOnzKL0U_UjzzS4PoGf4GKK8AQ@mail.gmail.com","threadId":"66022","inReplyTo":"CABPp-BFfF+Vd6RY3pG=FVUH_93YZULfhcXdWCv6zcRSABfGBQQ@mail.gmail.com","subject":"Re: [PATCH v2 11/12] fast-import: use parse_options() for command line options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:32:27Z","receivedAt":"2026-08-11T08:32:40Z","isPatch":true,"body":"On Sat, Aug 8, 2026 at 9:25 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Tue, Aug 4, 2026 at 3:04 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n> >\n> > Previous commits have started to use the parse-options API to display\n> > output from `git fast-import -h` and `git fast-import --help-all` and\n> > to prepare for parsing the command line options using this API.\n> >\n> > Let's now actually use the API to parse command line options.\n> >\n> > This brings a number of changes that are mostly beneficial:\n> >\n> >   - The `--alias`, `--get-mark`, `--cat-blob`, `--ls` and `--notes`\n> >     options are no longer accepted on the command line. They were\n> >     previously accepted as no-ops because parse_argv() fell through to\n> >     parse_one_feature(). They are not documented in the OPTIONS section\n> >     and are only meaningful as in-stream feature assertions, so\n> >     accepting them on the command line was an accident of code sharing\n> >     dating back to 9c8398f0c9 (fast-import: add option command,\n> >     2009-12-04).\n> >\n> >   - Abbreviated options like `--dep=5` now work since parse_options()\n> >     allows unambiguous prefixes.\n> >\n> >   - As `--cat-blob` is an abbreviation of `--cat-blob-fd`, using the\n> >     former on the command line will fail with \"option `cat-blob-fd'\n> >     requires a value\" unlike the other four options that are not\n> >     accepted anymore on the command line (see above).\n> >\n> >   - The error messages for some options might differ a bit.\n> >\n> >   - The code is shorter and more standard.\n>\n> I think the list might be missing three behavioral changes:\n>\n> 1) A bare \"--\" (or a trailing \"--\") is now accepted and the command\n> reads the stream normally, whereas the base treated it as a usage\n> error:\n>\n>         printf '' | git fast-import --   # before: 129 (usage), after: 0\n>\n> The old parse_argv() broke on \"--\" and then did \"if (i != state->argc)\n> usage_with_options(...)\"; parse_options() instead consumes \"--\" as the\n> end-of-options marker and returns just argv0.  Harmless /\n> conventional, just unlisted.\n>\n> 2) Value-taking options now also accept the space-separated \"--opt\n> value\" form (e.g. \"--depth 5\", \"--max-pack-size 1m\", \"--date-format\n> raw\"), not just \"--opt=value\".   Also expected parse_options()\n> behavior and a nice improvement.\n\nYeah, in v3 I have added the following to the commit message:\n\n      - Value-taking options now also accept the space-separated\n        `--opt value` form, like `--depth 5`, in addition to the\n        `--opt=value` form.\n\n      - A bare or trailing `--` is now accepted and the stream is read\n        normally, while it used to be a usage error.\n\n> 3) The handling of \"--allow-unsafe-features\" has changed and might\n> trip users up.\n>\n> Because the \"feature\" lines at the top of the stream are processed\n> before parse_argv() runs, cmd_fast_import() does an early scan just to\n> learn whether unsafe features are permitted:\n>\n>         for (int i = 1; i < argc; i++) {\n>                 const char *arg = argv[i];\n>                 if (*arg != '-' || !strcmp(arg, \"--\"))\n>                         break;\n>                 if (!strcmp(arg, \"--allow-unsafe-features\"))\n>                         state.allow_unsafe_features = 1;\n>         }\n>\n> That scan (a) matches only the exact spelling and (b) stops at the\n> first token not starting with '-'.  In the base that was fine, because\n> the old parse_argv loop was equally strict (exact spelling,\n> \"--opt=value\" only).\n> But now that parse_options() also accepts unambiguous abbreviations\n> and space-separated values, the two passes disagree.  With an unsafe\n> feature line in the stream:\n>\n>     # (A) old-style spelling, still fine:\n>     printf 'feature import-marks-if-exists=/nope\\n' |\n>       git fast-import --depth=5 --allow-unsafe-features\n>     # -> exit 0\n>\n>     # (B) space-separated value, newly accepted by parse_options():\n>     printf 'feature import-marks-if-exists=/nope\\n' |\n>       git fast-import --depth 5 --allow-unsafe-features\n>     # -> fatal: feature 'import-marks-if-exists' forbidden ... (128)\n>\n>     # (C) abbreviation, newly accepted by parse_options():\n>     printf 'feature import-marks-if-exists=/nope\\n' |\n>       git fast-import --allow-unsafe\n>     # -> fatal: feature 'import-marks-if-exists' forbidden ... (128)\n>\n> In (B) the early scan breaks on the bare \"5\" (it can't tell \"5\" is\n> --depth's argument) and never reaches --allow-unsafe-features; in (C)\n> the abbreviation isn't recognized by the strcmp().  Yet in both cases\n> parse_options() itself accepts the option (\"git fast-import\n> --allow-unsafe\" alone exits 0), so it's only the in-stream feature\n> that gets rejected.\n>\n> This errs on the safe side (it refuses an unsafe feature rather than\n> allowing one), and it's a minor inconsistency, but it might surprise\n> users.  At a minimum, it should probably be documented as a\n> shortcoming or TODO or something.  One possible solution is a\n> dedicated parse_options() pass for just --allow-unsafe-features;\n> another might be just requiring --allow-unsafe-features to be the\n> *first* argument.  Thoughts?\n\nRight, I prefer to document it for now while I plan to work on fixing\nthis in a more general way soon, as there are other places that have\nsimilar problems.\n\nIn v3, I have added the following to the `--allow-unsafe-features`\noption documentation in \"Documentation/git-fast-import.adoc\":\n\n    Note that this option has to be spelled in full, and has to appear\n    before any option whose value is separated from it by a space, for\n    the unsafe `feature` commands in the stream to be allowed. So\n    `--allow-unsafe` or `--depth 5 --allow-unsafe-features` still refuse\n    them, while `--allow-unsafe-features --depth 5` and\n    `--depth=5 --allow-unsafe-features` allow them.\n\nand this NEEDSWORK in the code:\n\n   * NEEDSWORK: This scan only matches the exact \"--allow-unsafe-features\"\n   * spelling and stops at the first argument that doesn't start with a\n   * dash. As parse_options() below also accepts unambiguous abbreviations\n   * and values separated by a space from their option, the two disagree\n   * for command lines like \"--allow-unsafe\" or \"--depth 5\n   * --allow-unsafe-features\": parse_options() accepts the option, but\n   * this scan doesn't see it, so unsafe features from the stream are\n   * still refused. This errs on the safe side, but should be fixed by\n   * teaching this scan about the options that take a value.\n\n> Anyway, other than the above list of three additional behavioral\n> changes, the rest of the patch looks right.  Thanks for tackling\n> modernizing this command.\n\nThank you for the review.\n"},{"id":"550249","messageId":"20260811083314.2023489-1-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260804100355.1299498-1-christian.couder@gmail.com","subject":"[PATCH v3 00/12] fast-import: standardize usage string and SYNOPSIS","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:02Z","receivedAt":"2026-08-11T08:33:32Z","isPatch":true,"body":"The goal of this series is to improve on `git fast-import`'s usage\nstring as it is obsolete in many ways.\n\nAs it appeared that a good way to reach that goal was to make\n`git fast-import` use the parse-options API, this series also achieves\nthis secondary goal.\n\nAlong the way it modernizes \"builtin/fast-import.c\" mostly by using\n`struct option`, by starting to remove global variables and libify\nthat command, and by introducing a new `OPT_HIDDEN_GROUP` macro.\n\nThere are still many global variables left, so it's left to future\nwork to finish on that direction.\n\nAnyway the usage string is standardized and consistent with the\nSYNOPSIS in the docs, so that the command can be removed from\n\"t/t0450/adoc-help-mismatches\".\n\nUsing the parse-options API also enabled some code standardization and\nsimplification.\n\nOverview of the patches\n=======================\n\n  - Patch 1/12: Introduces OPT_HIDDEN_GROUP and improves on the hidden\n    option tests.\n\n  - Patches 2/12 and 3/12: Improve on the parse-options API docs.\n\n  - Patch 4/12: Cleans up an 'i' loop counter variable in\n    cmd_fast_import().\n\n  - Patches 5/12 and 6/12: Prepare for using the parse-option API to\n    parse command line options.\n\n  - Patches 7/12 and 8/12: Start libifying \"builtin/fast-import.c\" by\n    introducing a 'struct fast_import_state' and using it to store\n    some global variables.\n\n  - Patch 9/12: Improves the usage string and SYNOPSIS by introducing\n    `struct option`.\n\n  - Patch 10/12: Further prepares for using the parse-option API to\n    actually parse command line options by using OPT_CALLBACK.\n\n  - Patch 11/12: Actually parses the command line options using the\n    parse-option API.\n\n  - Patch 12/12: Performs a cleanup of some functions arguments allowed\n    by the previous commit.\n\nChanges since v2\n================\n\nThanks to Elijah for reviewing v2 and Junio for reviewing v1.\n\nThe series has been rebased on top of recent master at 010afd3166 (The\n12th batch, 2026-08-07).\n\n  - Patch 2/12 now describes `PARSE_OPT_LASTARG_DEFAULT` the way Elijah\n    suggested, which matches what \"parse-options.h\" says, and mentions\n    that it should not be combined with `PARSE_OPT_OPTARG`.\n\n  - Patch 7/12 now wraps the function signatures that grew too long\n    when they gained a 'struct fast_import_state *state' argument, with\n    one parameter per line. Patches 8/12 and 12/12 have been adjusted\n    accordingly, as they touch two of these signatures again.\n\n  - Patch 11/12 now also lists two other behavior changes that Elijah\n    spotted: value-taking options accept the space-separated\n    `--opt value` form, and a bare or trailing `--` is now accepted.\n\n  - Patch 11/12 also documents, both in a NEEDSWORK code comment and\n    in \"Documentation/git-fast-import.adoc\", the third behavior change\n    Elijah spotted about `--allow-unsafe-features`: as the early scan\n    for that option only matches its exact spelling and stops at the\n    first non-option argument, it disagrees with parse_options() for\n    command lines like `--allow-unsafe` or\n    `--depth 5 --allow-unsafe-features`. This errs on the safe side,\n    as unsafe `feature` commands from the stream are refused in that\n    case.\n\n    I plan to work on a follow-up series that will improve on this by\n    teaching the early scan about the options that take a value. It\n    looks like the parse-options API could provide some helpers for\n    this, which might benefit other commands like `git` itself (see\n    handle_options() in \"git.c\") that need to look at some options\n    before the actual option parsing.\n\nCI tests\n========\n\nThey all pass, see:\n\nhttps://github.com/chriscool/git/actions/runs/31469672131\n\nChristian Couder (12):\n  parse-options: introduce OPT_HIDDEN_GROUP\n  api-parse-options.adoc: document per-option flags\n  api-parse-options.adoc: document hidden and OPT_*_F option macros\n  fast-import: localize 'i' into the 'for' loops using it\n  fast-import: use int for some bool flags\n  fast-import: factor out option_*() functions\n  fast-import: introduce 'struct fast_import_state'\n  fast-import: move command state globals into 'struct\n    fast_import_state'\n  fast-import: use struct option for usage string\n  fast-import: use callbacks to parse some options\n  fast-import: use parse_options() for command line options\n  fast-import: remove useless from_stream argument\n\n Documentation/git-fast-import.adoc            |   9 +-\n .../technical/api-parse-options.adoc          |  80 +++\n builtin/fast-import.c                         | 617 ++++++++++++------\n parse-options.c                               |   4 +-\n parse-options.h                               |   5 +\n t/helper/test-parse-options.c                 |   4 +\n t/t0040-parse-options.sh                      |  25 +-\n t/t0450/adoc-help-mismatches                  |   1 -\n t/t9300-fast-import.sh                        |   7 +\n 9 files changed, 564 insertions(+), 188 deletions(-)\n\nRange-diff against v2:\n 1:  b09d727e71 =  1:  06e37fe78b parse-options: introduce OPT_HIDDEN_GROUP\n 2:  fe21471420 !  2:  184837437c api-parse-options.adoc: document per-option flags\n    @@ Documentation/technical/api-parse-options.adoc: Data Structure\n     +\tdeprecated, advanced or otherwise uncommon options.\n     +\n     +`PARSE_OPT_LASTARG_DEFAULT`::\n    -+\tUse the default value (`defval`) when the option is used\n    -+\twithout an argument, even for an option that normally requires\n    -+\tone. Only the last argument on the command line takes effect.\n    ++\tThe no-argument form is only accepted when the option is the\n    ++\tlast token on the command line; used earlier, it still\n    ++\trequires an argument. Should not be combined with\n    ++\t`PARSE_OPT_OPTARG`.\n     +\n     +`PARSE_OPT_NODASH`::\n     +\tThe option is a single character without a leading dash, such\n 3:  f43d4cefa4 =  3:  3e3655c6d8 api-parse-options.adoc: document hidden and OPT_*_F option macros\n 4:  3eb8106ed6 =  4:  bb0dc9b12e fast-import: localize 'i' into the 'for' loops using it\n 5:  34514eb2ac =  5:  e653d2a43f fast-import: use int for some bool flags\n 6:  336395c70a =  6:  2f809b3a01 fast-import: factor out option_*() functions\n 7:  45cc2107e2 !  7:  3c913f0034 fast-import: introduce 'struct fast_import_state'\n    @@ builtin/fast-import.c: static kh_oid_map_t *sub_oid_map;\n     +static void parse_argv(struct fast_import_state *state);\n     +static void parse_get_mark(struct fast_import_state *state, const char *p);\n     +static void parse_cat_blob(struct fast_import_state *state, const char *p);\n    -+static void parse_ls(struct fast_import_state *state, const char *p, struct branch *b);\n    ++static void parse_ls(struct fast_import_state *state,\n    ++\t\t     const char *p,\n    ++\t\t     struct branch *b);\n      \n      static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p)\n      {\n    @@ builtin/fast-import.c: static void parse_path_space(struct strbuf *sb, const cha\n      }\n      \n     -static void file_change_m(const char *p, struct branch *b)\n    -+static void file_change_m(struct fast_import_state *state, const char *p, struct branch *b)\n    ++static void file_change_m(struct fast_import_state *state,\n    ++\t\t\t  const char *p,\n    ++\t\t\t  struct branch *b)\n      {\n      \tstatic struct strbuf path = STRBUF_INIT;\n      \tstruct object_entry *oe;\n    @@ builtin/fast-import.c: static void file_change_cr(const char *p, struct branch *\n      }\n      \n     -static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n    -+static void note_change_n(struct fast_import_state *state, const char *p, struct branch *b, unsigned char *old_fanout)\n    ++static void note_change_n(struct fast_import_state *state,\n    ++\t\t\t  const char *p,\n    ++\t\t\t  struct branch *b,\n    ++\t\t\t  unsigned char *old_fanout)\n      {\n      \tstruct object_entry *oe;\n      \tstruct branch *s;\n    @@ builtin/fast-import.c: static void parse_from_existing(struct branch *b)\n      }\n      \n     -static int parse_objectish(struct branch *b, const char *objectish)\n    -+static int parse_objectish(struct fast_import_state *state, struct branch *b, const char *objectish)\n    ++static int parse_objectish(struct fast_import_state *state,\n    ++\t\t\t   struct branch *b,\n    ++\t\t\t   const char *objectish)\n      {\n      \tstruct branch *s;\n      \tstruct object_id oid;\n    @@ builtin/fast-import.c: static int parse_objectish(struct branch *b, const char *\n      }\n      \n     -static int parse_objectish_with_prefix(struct branch *b, const char *prefix)\n    -+static int parse_objectish_with_prefix(struct fast_import_state *state, struct branch *b, const char *prefix)\n    ++static int parse_objectish_with_prefix(struct fast_import_state *state,\n    ++\t\t\t\t       struct branch *b,\n    ++\t\t\t\t       const char *prefix)\n      {\n      \tconst char *base;\n      \n    @@ builtin/fast-import.c: static int parse_objectish(struct branch *b, const char *\n      }\n      \n     -static struct hash_list *parse_merge(unsigned int *count)\n    -+static struct hash_list *parse_merge(struct fast_import_state *state, unsigned int *count)\n    ++static struct hash_list *parse_merge(struct fast_import_state *state,\n    ++\t\t\t\t     unsigned int *count)\n      {\n      \tstruct hash_list *list = NULL, **tail = &list, *n;\n      \tconst char *from;\n    @@ builtin/fast-import.c: struct signature_data {\n      };\n      \n     -static void parse_one_signature(struct signature_data *sig, const char *v)\n    -+static void parse_one_signature(struct fast_import_state *state, struct signature_data *sig, const char *v)\n    ++static void parse_one_signature(struct fast_import_state *state,\n    ++\t\t\t\tstruct signature_data *sig,\n    ++\t\t\t\tconst char *v)\n      {\n      \tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n      \tchar *space = strchr(args, ' ');\n    @@ builtin/fast-import.c: static void cat_blob(struct object_entry *oe, struct obje\n      }\n      \n     -static void parse_get_mark(const char *p)\n    -+static void parse_get_mark(struct fast_import_state *state UNUSED, const char *p)\n    ++static void parse_get_mark(struct fast_import_state *state UNUSED,\n    ++\t\t\t   const char *p)\n      {\n      \tstruct object_entry *oe;\n      \tchar output[GIT_MAX_HEXSZ + 2];\n    @@ builtin/fast-import.c: static void parse_get_mark(const char *p)\n      }\n      \n     -static void parse_cat_blob(const char *p)\n    -+static void parse_cat_blob(struct fast_import_state *state UNUSED, const char *p)\n    ++static void parse_cat_blob(struct fast_import_state *state UNUSED,\n    ++\t\t\t   const char *p)\n      {\n      \tstruct object_entry *oe;\n      \tstruct object_id oid;\n    @@ builtin/fast-import.c: static void print_ls(int mode, const unsigned char *hash,\n      }\n      \n     -static void parse_ls(const char *p, struct branch *b)\n    -+static void parse_ls(struct fast_import_state *state UNUSED, const char *p, struct branch *b)\n    ++static void parse_ls(struct fast_import_state *state UNUSED,\n    ++\t\t     const char *p,\n    ++\t\t     struct branch *b)\n      {\n      \tstatic struct strbuf path = STRBUF_INIT;\n      \tstruct tree_entry *root = NULL;\n    @@ builtin/fast-import.c: static void parse_progress(void)\n      }\n      \n     -static char* make_fast_import_path(const char *path)\n    -+static char* make_fast_import_path(struct fast_import_state *state, const char *path)\n    ++static char* make_fast_import_path(struct fast_import_state *state,\n    ++\t\t\t\t   const char *path)\n      {\n      \tif (!relative_marks_paths || is_absolute_path(path))\n     -\t\treturn prefix_filename(global_prefix, path);\n    @@ builtin/fast-import.c: static void parse_progress(void)\n      }\n      \n     -static void option_import_marks(const char *marks,\n    -+static void option_import_marks(struct fast_import_state *state, const char *marks,\n    - \t\t\t\t\tint from_stream, int ignore_missing)\n    +-\t\t\t\t\tint from_stream, int ignore_missing)\n    ++static void option_import_marks(struct fast_import_state *state,\n    ++\t\t\t\tconst char *marks,\n    ++\t\t\t\tint from_stream,\n    ++\t\t\t\tint ignore_missing)\n      {\n      \tif (import_marks_file) {\n    + \t\tif (from_stream)\n     @@ builtin/fast-import.c: static void option_import_marks(const char *marks,\n      \t}\n      \n    @@ builtin/fast-import.c: static void option_active_branches(const char *branches)\n      }\n      \n     -static void option_export_marks(const char *marks)\n    -+static void option_export_marks(struct fast_import_state *state, const char *marks)\n    ++static void option_export_marks(struct fast_import_state *state,\n    ++\t\t\t\tconst char *marks)\n      {\n      \tfree(export_marks_file);\n     -\texport_marks_file = make_fast_import_path(marks);\n    @@ builtin/fast-import.c: static void option_active_branches(const char *branches)\n      }\n      \n     -static void option_cat_blob_fd(const char *fd)\n    -+static void option_cat_blob_fd(struct fast_import_state *state UNUSED, const char *fd)\n    ++static void option_cat_blob_fd(struct fast_import_state *state UNUSED,\n    ++\t\t\t       const char *fd)\n      {\n      \tunsigned long n = ulong_arg(\"--cat-blob-fd\", fd);\n      \tif (n > (unsigned long) INT_MAX)\n    @@ builtin/fast-import.c: static void option_cat_blob_fd(const char *fd)\n      }\n      \n     -static void option_export_pack_edges(const char *edges)\n    -+static void option_export_pack_edges(struct fast_import_state *state, const char *edges)\n    ++static void option_export_pack_edges(struct fast_import_state *state,\n    ++\t\t\t\t     const char *edges)\n      {\n     -\tchar *fn = prefix_filename(global_prefix, edges);\n     +\tchar *fn = prefix_filename(state->prefix, edges);\n    @@ builtin/fast-import.c: static void option_cat_blob_fd(const char *fd)\n      }\n      \n     -static void option_rewrite_submodules(const char *arg, struct string_list *list)\n    -+static void option_rewrite_submodules(struct fast_import_state *state, const char *arg, struct string_list *list)\n    ++static void option_rewrite_submodules(struct fast_import_state *state,\n    ++\t\t\t\t      const char *arg,\n    ++\t\t\t\t      struct string_list *list)\n      {\n      \tstruct mark_set *ms;\n      \tFILE *fp;\n    @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n      }\n      \n     -static void check_unsafe_feature(const char *feature, int from_stream)\n    -+static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n    ++static void check_unsafe_feature(struct fast_import_state *state UNUSED,\n    ++\t\t\t\t const char *feature,\n    ++\t\t\t\t int from_stream)\n      {\n      \tif (from_stream && !allow_unsafe_features)\n      \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n    @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n      }\n      \n     -static int parse_one_feature(const char *feature, int from_stream)\n    -+static int parse_one_feature(struct fast_import_state *state, const char *feature, int from_stream)\n    ++static int parse_one_feature(struct fast_import_state *state,\n    ++\t\t\t     const char *feature,\n    ++\t\t\t     int from_stream)\n      {\n      \tconst char *arg;\n      \n 8:  7f068facc2 !  8:  5d0b07148e fast-import: move command state globals into 'struct fast_import_state'\n    @@ builtin/fast-import.c: static int parse_one_option(struct fast_import_state *sta\n      \treturn 1;\n      }\n      \n    --static void check_unsafe_feature(struct fast_import_state *state UNUSED, const char *feature, int from_stream)\n    -+static void check_unsafe_feature(struct fast_import_state *state, const char *feature, int from_stream)\n    +-static void check_unsafe_feature(struct fast_import_state *state UNUSED,\n    ++static void check_unsafe_feature(struct fast_import_state *state,\n    + \t\t\t\t const char *feature,\n    + \t\t\t\t int from_stream)\n      {\n     -\tif (from_stream && !allow_unsafe_features)\n     +\tif (from_stream && !state->allow_unsafe_features)\n      \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n      \t\t    feature);\n      }\n    -@@ builtin/fast-import.c: static int parse_one_feature(struct fast_import_state *state, const char *featur\n    +@@ builtin/fast-import.c: static int parse_one_feature(struct fast_import_state *state,\n      \n      static void parse_feature(struct fast_import_state *state, const char *feature)\n      {\n 9:  3d6ab86518 =  9:  efcd1b9ac4 fast-import: use struct option for usage string\n10:  3208937f13 = 10:  d109b8c622 fast-import: use callbacks to parse some options\n11:  202a50beec ! 11:  95919e8319 fast-import: use parse_options() for command line options\n    @@ Commit message\n             requires a value\" unlike the other four options that are not\n             accepted anymore on the command line (see above).\n     \n    +      - Value-taking options now also accept the space-separated\n    +        `--opt value` form, like `--depth 5`, in addition to the\n    +        `--opt=value` form.\n    +\n    +      - A bare or trailing `--` is now accepted and the stream is read\n    +        normally, while it used to be a usage error.\n    +\n           - The error messages for some options might differ a bit.\n     \n           - The code is shorter and more standard.\n    @@ Commit message\n     \n         Signed-off-by: Christian Couder <christian.couder@gmail.com>\n     \n    + ## Documentation/git-fast-import.adoc ##\n    +@@ Documentation/git-fast-import.adoc: Only enable this option if you trust the program generating the\n    + fast-import stream! This option is enabled automatically for\n    + remote-helpers that use the `import` capability, as they are\n    + already trusted to run their own code.\n    +++\n    ++Note that this option has to be spelled in full, and has to appear\n    ++before any option whose value is separated from it by a space, for\n    ++the unsafe `feature` commands in the stream to be allowed. So\n    ++`--allow-unsafe` or `--depth 5 --allow-unsafe-features` still refuse\n    ++them, while `--allow-unsafe-features --depth 5` and\n    ++`--depth=5 --allow-unsafe-features` allow them.\n    + \n    + `--signed-tags=<mode>`::\n    + \tSpecify how to handle signed tags. Behaves in the same way as\n    +\n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: static const char *const fast_import_usage[] = {\n      \n    @@ builtin/fast-import.c: static const char *const fast_import_usage[] = {\n     -\n     -\t\tif (*a != '-' || !strcmp(a, \"--\"))\n     -\t\t\tbreak;\n    -+\tint argc = parse_options(state->argc, state->argv, state->prefix,\n    -+\t\t\t\t state->option, fast_import_usage,\n    -+\t\t\t\t PARSE_OPT_KEEP_ARGV0);\n    - \n    +-\n     -\t\tif (!skip_prefix(a, \"--\", &a))\n     -\t\t\tdie(_(\"unknown option %s\"), a);\n     -\n    @@ builtin/fast-import.c: static const char *const fast_import_usage[] = {\n     -\t\t\toption_cat_blob_fd(state, a);\n     -\t\t\tcontinue;\n     -\t\t}\n    --\n    ++\tint argc = parse_options(state->argc, state->argv, state->prefix,\n    ++\t\t\t\t state->option, fast_import_usage,\n    ++\t\t\t\t PARSE_OPT_KEEP_ARGV0);\n    + \n     -\t\tdie(_(\"unknown option --%s\"), a);\n     -\t}\n     -\tif (i != state->argc)\n    @@ builtin/fast-import.c: int cmd_fast_import(int argc,\n      \tstruct option fast_import_options[] = {\n      \t\tOPT_GROUP(N_(\"Common\")),\n      \t\tOPT_CALLBACK_F(0, \"date-format\", NULL, N_(\"fmt\"),\n    +@@ builtin/fast-import.c: int cmd_fast_import(int argc,\n    + \t * \"feature\" lines at the start of the stream (which allows the command\n    + \t * line to override stream data). But we must do an early parse of any\n    + \t * command-line options that impact how we interpret the feature lines.\n    ++\t *\n    ++\t * NEEDSWORK: This scan only matches the exact \"--allow-unsafe-features\"\n    ++\t * spelling and stops at the first argument that doesn't start with a\n    ++\t * dash. As parse_options() below also accepts unambiguous abbreviations\n    ++\t * and values separated by a space from their option, the two disagree\n    ++\t * for command lines like \"--allow-unsafe\" or \"--depth 5\n    ++\t * --allow-unsafe-features\": parse_options() accepts the option, but\n    ++\t * this scan doesn't see it, so unsafe features from the stream are\n    ++\t * still refused. This errs on the safe side, but should be fixed by\n    ++\t * teaching this scan about the options that take a value.\n    + \t */\n    + \tfor (int i = 1; i < argc; i++) {\n    + \t\tconst char *arg = argv[i];\n     \n      ## t/t9300-fast-import.sh ##\n     @@ t/t9300-fast-import.sh: test_expect_success 'R: unknown commandline options are rejected' '\\\n12:  99ff791a62 ! 12:  96f17c83d2 fast-import: remove useless from_stream argument\n    @@ Commit message\n     \n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: static int parse_one_option(struct fast_import_state *state, const char *option)\n    - \treturn 1;\n      }\n      \n    --static void check_unsafe_feature(struct fast_import_state *state, const char *feature, int from_stream)\n    -+static void check_unsafe_feature(struct fast_import_state *state, const char *feature)\n    + static void check_unsafe_feature(struct fast_import_state *state,\n    +-\t\t\t\t const char *feature,\n    +-\t\t\t\t int from_stream)\n    ++\t\t\t\t const char *feature)\n      {\n     -\tif (from_stream && !state->allow_unsafe_features)\n     +\tif (!state->allow_unsafe_features)\n    @@ builtin/fast-import.c: static int parse_one_option(struct fast_import_state *sta\n      \t\t    feature);\n      }\n      \n    --static int parse_one_feature(struct fast_import_state *state, const char *feature, int from_stream)\n    -+static int parse_one_feature(struct fast_import_state *state, const char *feature)\n    + static int parse_one_feature(struct fast_import_state *state,\n    +-\t\t\t     const char *feature,\n    +-\t\t\t     int from_stream)\n    ++\t\t\t     const char *feature)\n      {\n      \tconst char *arg;\n      \n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550250","messageId":"20260811083314.2023489-3-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 02/12] api-parse-options.adoc: document per-option flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:04Z","receivedAt":"2026-08-11T08:33:35Z","isPatch":true,"body":"The \"Flags\" section in \"Documentation/technical/api-parse-options.adoc\"\ndocuments the flags that can be passed to parse_options() itself. It\ndoes not, however, document the flags that can be set on individual\noptions through the `flags` member of `struct option` (and through the\n`OPT_*_F()` macro variants).\n\nThese per-option flags are used throughout the codebase (for example\n`PARSE_OPT_HIDDEN` is used to hide an option from `-h` while still\nshowing it with `--help-all`), but a reader currently has to dig into\n\"parse-options.h\" to find them.\n\nTo remediate that, let's add an \"Option flags\" subsection to the\n\"Data Structure\" section, just before the list of option macros.\n\nLet's also make it explicit that these are distinct from the\nparse_options() flags described earlier, and let's describe the `-h`\nversus `--help-all` behavior for `PARSE_OPT_HIDDEN`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n .../technical/api-parse-options.adoc          | 62 +++++++++++++++++++\n 1 file changed, 62 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex 880eb94642..5602cd44b2 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -150,6 +150,68 @@ Data Structure\n \n The main data structure is an array of the `option` struct,\n say `static struct option builtin_add_options[]`.\n+\n+Option flags\n+~~~~~~~~~~~~\n+\n+Each option can carry flags in the `flags` field of its `option`\n+struct. These are per-option flags and are distinct from the\n+`parse_options()` flags described above; they are usually set through\n+the `OPT_*_F()` macro variants (see below) rather than by hand. They\n+are the bitwise-or of:\n+\n+`PARSE_OPT_OPTARG`::\n+\tThe option's argument is optional, i.e. both `--option` and\n+\t`--option=<value>` are accepted.\n+\n+`PARSE_OPT_NOARG`::\n+\tThe option takes no argument at all. Using `--option=<value>`\n+\tis rejected.\n+\n+`PARSE_OPT_NONEG`::\n+\tDisable the automatically generated negated `--no-option`\n+\tform.\n+\n+`PARSE_OPT_HIDDEN`::\n+\tHide the option: it is omitted from the usage shown by\n+\t`git <cmd> -h`, but is still shown by `git <cmd> --help-all`.\n+\tThe option is parsed as usual either way. This is meant for\n+\tdeprecated, advanced or otherwise uncommon options.\n+\n+`PARSE_OPT_LASTARG_DEFAULT`::\n+\tThe no-argument form is only accepted when the option is the\n+\tlast token on the command line; used earlier, it still\n+\trequires an argument. Should not be combined with\n+\t`PARSE_OPT_OPTARG`.\n+\n+`PARSE_OPT_NODASH`::\n+\tThe option is a single character without a leading dash, such\n+\tas the `+` used by some commands.\n+\n+`PARSE_OPT_LITERAL_ARGHELP`::\n+\tUse the argument help string (`argh`) verbatim in the usage\n+\toutput instead of surrounding it with `<>` or `[]`. Useful when\n+\t`argh` already contains a hand-formatted description.\n+\n+`PARSE_OPT_FROM_ALIAS`::\n+\tInternal flag, set on options that were expanded from a\n+\tconfigured alias. It should not be set by callers.\n+\n+`PARSE_OPT_NOCOMPLETE`::\n+\tDo not offer this option for completion.\n+\n+`PARSE_OPT_COMP_ARG`::\n+\tThe option's argument, rather than the option itself, is what\n+\tshould be completed.\n+\n+`PARSE_OPT_CMDMODE`::\n+\tThe option is one of several mutually exclusive \"command mode\"\n+\toptions that share the same variable. Using more than one of\n+\tthem at once is rejected.\n+\n+Macros\n+~~~~~~\n+\n There are some macros to easily define options:\n \n `OPT__ABBREV(&int_var)`::\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550251","messageId":"20260811083314.2023489-4-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 03/12] api-parse-options.adoc: document hidden and OPT_*_F option macros","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:05Z","receivedAt":"2026-08-11T08:33:37Z","isPatch":true,"body":"In \"Documentation/technical/api-parse-options.adoc\", the list of option\nmacros does not mention the `OPT_*_F()` macro variants that take a\ntrailing `flags` argument, nor the `OPT_HIDDEN_GROUP()` and\n`OPT_HIDDEN_BOOL()` convenience macros.\n\nNow that a previous commit documents the per-option flags, let's\ndocument these macros too:\n\n  - Add a paragraph explaining the `OPT_*_F` convention and how it\n    relates to the per-option flags.\n\n  - Document `OPT_HIDDEN_GROUP()`, introduced in a previous commit,\n    right after `OPT_GROUP()`.\n\n  - Document `OPT_HIDDEN_BOOL()` right after `OPT_BOOL()`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n Documentation/technical/api-parse-options.adoc | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc\nindex 5602cd44b2..95b7924e84 100644\n--- a/Documentation/technical/api-parse-options.adoc\n+++ b/Documentation/technical/api-parse-options.adoc\n@@ -214,6 +214,13 @@ Macros\n \n There are some macros to easily define options:\n \n+Many of the macros below have an `_F` variant (for example `OPT_BOOL_F`,\n+`OPT_STRING_F`, `OPT_INTEGER_F`, `OPT_SET_INT_F`, `OPT_BIT_F` and\n+`OPT_CALLBACK_F`) that takes an additional trailing `flags` argument.\n+That argument is the bitwise-or of the per-option flags described in the\n+\"Option flags\" section above; the non-`_F` macros are simply defined\n+with `flags` set to `0`.\n+\n `OPT__ABBREV(&int_var)`::\n \tAdd `--abbrev[=<n>]`.\n \n@@ -237,10 +244,21 @@ There are some macros to easily define options:\n \tdescribes the group or an empty string.\n \tStart the description with an upper-case letter.\n \n+`OPT_HIDDEN_GROUP(description)`::\n+\tLike `OPT_GROUP()`, but the group header carries\n+\t`PARSE_OPT_HIDDEN`, so it is only shown by `--help-all` and not\n+\tby `-h`. Use it to label a group that contains only hidden\n+\toptions, which would otherwise show an empty header under `-h`.\n+\n `OPT_BOOL(short, long, &int_var, description)`::\n \tIntroduce a boolean option. `int_var` is set to one with\n \t`--option` and set to zero with `--no-option`.\n \n+`OPT_HIDDEN_BOOL(short, long, &int_var, description)`::\n+\tLike `OPT_BOOL()`, but the option carries `PARSE_OPT_HIDDEN`,\n+\tso it is hidden from `-h` while still being shown by\n+\t`--help-all`.\n+\n `OPT_COUNTUP(short, long, &int_var, description)`::\n \tIntroduce a count-up option.\n \tEach use of `--option` increments `int_var`, starting from zero\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550252","messageId":"20260811083314.2023489-5-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 04/12] fast-import: localize 'i' into the 'for' loops using it","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:06Z","receivedAt":"2026-08-11T08:33:38Z","isPatch":true,"body":"In cmd_fast_import(), a\tlocal variable 'i' is defined as an\n`unsigned int` and then used as a loop counter in four different\n`for (i = ...; i < ...; i++) { ... }` loops.\n\nBut in three out of the four cases, `unsigned int` isn't the best type\nto use.\n\nTo give each loop counter the type matching its bound\n(int/unsigned/size_t), let's localize 'i' into each loop that uses it.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 10 ++++------\n 1 file changed, 4 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 6692f7cd81..9fc9ebe65a 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3937,8 +3937,6 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n-\tunsigned int i;\n-\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3959,7 +3957,7 @@ int cmd_fast_import(int argc,\n \t * line to override stream data). But we must do an early parse of any\n \t * command-line options that impact how we interpret the feature lines.\n \t */\n-\tfor (i = 1; i < argc; i++) {\n+\tfor (int i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n@@ -3972,7 +3970,7 @@ int cmd_fast_import(int argc,\n \tglobal_prefix = prefix;\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n-\tfor (i = 0; i < (cmd_save - 1); i++)\n+\tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n \trc_free[cmd_save - 1].next = NULL;\n \n@@ -4035,9 +4033,9 @@ int cmd_fast_import(int argc,\n \n \tif (show_stats) {\n \t\tuintmax_t total_count = 0, duplicate_count = 0;\n-\t\tfor (i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(object_count_by_type); i++)\n \t\t\ttotal_count += object_count_by_type[i];\n-\t\tfor (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++)\n \t\t\tduplicate_count += duplicate_count_by_type[i];\n \n \t\tfprintf(stderr, \"%s statistics:\\n\", argv[0]);\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550253","messageId":"20260811083314.2023489-6-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 05/12] fast-import: use int for some bool flags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:07Z","receivedAt":"2026-08-11T08:33:40Z","isPatch":true,"body":"The `show_stats` and `quiet` flags are meant to be parsed and used as\nboolean flags.\n\nTo easily parse them using OPT_BOOL in a following commit, let's change\ntheir type from 'unsigned int' to just 'int'.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 9fc9ebe65a..9c8edd7c89 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -182,8 +182,8 @@ static unsigned long branch_count;\n static unsigned long branch_load_count;\n static int failure;\n static FILE *pack_edges;\n-static unsigned int show_stats = 1;\n-static unsigned int quiet;\n+static int show_stats = 1;\n+static int quiet;\n static int global_argc;\n static const char **global_argv;\n static const char *global_prefix;\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550254","messageId":"20260811083314.2023489-7-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 06/12] fast-import: factor out option_*() functions","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:08Z","receivedAt":"2026-08-11T08:33:42Z","isPatch":true,"body":"In a following commit we are going to use the parse-options API to\nstart parsing options. Some options will have to be parsed using\nOPT_CALLBACK as they process their arguments in special ways.\n\nWhen the processing code is already factored out in an option_*()\nfunction, like for `--date-format`, we can reuse that function.\nUnfortunately for other options the processing code has not been\nfactored out yet.\n\nLet's do it now and factor out the code that handles the following\noptions:\n\n  - `--max-pack-size=<n>`\n  - `--big-file-threshold=<n>`\n  - `--signed-commits=<mode>`\n  - `--signed-tags=<mode>`\n  - `--quiet`\n\ninto new option_*() functions:\n\n  - option_max_pack_size()\n  - option_big_file_threshold()\n  - option_signed_commits()\n  - option_signed_tags()\n  - option_quiet()\n\nso that we can reuse these functions in following commits when the\nparse-option API will be used.\n\nNote that there are some behavior changes as we now die() with a\nproper error message when git_parse_ulong() cannot parse the argument\nfrom --max-pack-size or from --big-file-threshold. Previously we would\nend up calling die(\"unknown option\") instead.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 69 ++++++++++++++++++++++++++++++-------------\n 1 file changed, 48 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 9c8edd7c89..a6e3cc0033 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3751,25 +3751,55 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tfree(s);\n }\n \n+static void option_max_pack_size(const char *arg)\n+{\n+\tunsigned long v;\n+\n+\tif (!git_parse_ulong(arg, &v))\n+\t\tdie(_(\"--max-pack-size: argument must be a non-negative integer\"));\n+\tif (v < 8192) {\n+\t\twarning(_(\"max-pack-size is now in bytes, assuming --max-pack-size=%lum\"), v);\n+\t\tv *= 1024 * 1024;\n+\t} else if (v < 1024 * 1024) {\n+\t\twarning(_(\"minimum max-pack-size is 1 MiB\"));\n+\t\tv = 1024 * 1024;\n+\t}\n+\tmax_packsize = v;\n+}\n+\n+static void option_big_file_threshold(const char *arg)\n+{\n+\tunsigned long v;\n+\n+\tif (!git_parse_ulong(arg, &v))\n+\t\tdie(_(\"--big-file-threshold: argument must be a non-negative integer\"));\n+\trepo_settings_set_big_file_threshold(the_repository, v);\n+}\n+\n+static void option_signed_commits(const char *arg)\n+{\n+\tif (parse_sign_mode(arg, &signed_commit_mode, &signed_commit_keyid))\n+\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), arg);\n+}\n+\n+static void option_signed_tags(const char *arg)\n+{\n+\tif (parse_sign_mode(arg, &signed_tag_mode, &signed_tag_keyid))\n+\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), arg);\n+}\n+\n+static void option_quiet(void)\n+{\n+\tshow_stats = 0;\n+\tquiet = 1;\n+}\n+\n static int parse_one_option(const char *option)\n {\n \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n-\t\tunsigned long v;\n-\t\tif (!git_parse_ulong(option, &v))\n-\t\t\treturn 0;\n-\t\tif (v < 8192) {\n-\t\t\twarning(_(\"max-pack-size is now in bytes, assuming --max-pack-size=%lum\"), v);\n-\t\t\tv *= 1024 * 1024;\n-\t\t} else if (v < 1024 * 1024) {\n-\t\t\twarning(_(\"minimum max-pack-size is 1 MiB\"));\n-\t\t\tv = 1024 * 1024;\n-\t\t}\n-\t\tmax_packsize = v;\n+\t\toption_max_pack_size(option);\n \t} else if (skip_prefix(option, \"big-file-threshold=\", &option)) {\n-\t\tunsigned long v;\n-\t\tif (!git_parse_ulong(option, &v))\n-\t\t\treturn 0;\n-\t\trepo_settings_set_big_file_threshold(the_repository, v);\n+\t\toption_big_file_threshold(option);\n \t} else if (skip_prefix(option, \"depth=\", &option)) {\n \t\toption_depth(option);\n \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n@@ -3777,14 +3807,11 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))\n-\t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n+\t\toption_signed_commits(option);\n \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_tag_mode, &signed_tag_keyid))\n-\t\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), option);\n+\t\toption_signed_tags(option);\n \t} else if (!strcmp(option, \"quiet\")) {\n-\t\tshow_stats = 0;\n-\t\tquiet = 1;\n+\t\toption_quiet();\n \t} else if (!strcmp(option, \"stats\")) {\n \t\tshow_stats = 1;\n \t} else if (!strcmp(option, \"allow-unsafe-features\")) {\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550255","messageId":"20260811083314.2023489-8-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 07/12] fast-import: introduce 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:09Z","receivedAt":"2026-08-11T08:33:45Z","isPatch":true,"body":"\"builtin/fast-import.c\" uses a large number of global variables. This\nmakes it harder than necessary to reason about and improve. Especially\nadding new features requires adding more global variables, while\nmodernizing and eventually libifying the code becomes more and more\ndifficult.\n\nTo start reverting the sad trend to more and more globals and to start\ncleaning things up, let's introduce a 'struct fast_import_state' and\npass an instance of it as the first argument to many functions.\n\nThis is similar to what was done for \"builtin/apply.c\" by introducing a\n'struct apply_state', see 07d7e290ff (apply: move 'struct apply_state'\nto a header file, 2016-08-11) and related commits.\n\nAs a first step only the 'global_argc', 'global_argv' and\n'global_prefix' variables are moved into the new struct. More variables\nwill be moved into it in the following commits.\n\nSome functions receive the new 'state' parameter only to pass it\nalong or for future use, so they are marked with UNUSED for now to\nsatisfy '-Werror=unused-parameter'.\n\nThis is a mostly mechanical refactoring with no intended behavior\nchange.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 294 ++++++++++++++++++++++++------------------\n 1 file changed, 169 insertions(+), 125 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex a6e3cc0033..0f838d8488 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -184,10 +184,6 @@ static int failure;\n static FILE *pack_edges;\n static int show_stats = 1;\n static int quiet;\n-static int global_argc;\n-static const char **global_argv;\n-static const char *global_prefix;\n-\n static enum sign_mode signed_tag_mode = SIGN_VERBATIM;\n static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n static const char *signed_commit_keyid;\n@@ -276,10 +272,29 @@ static kh_oid_map_t *sub_oid_map;\n /* Where to write output of cat-blob commands */\n static int cat_blob_fd = STDOUT_FILENO;\n \n-static void parse_argv(void);\n-static void parse_get_mark(const char *p);\n-static void parse_cat_blob(const char *p);\n-static void parse_ls(const char *p, struct branch *b);\n+/* Command state */\n+struct fast_import_state {\n+\tint argc;\n+\tconst char **argv;\n+\tconst char *prefix;\n+};\n+\n+static void fast_import_state_init(struct fast_import_state *state,\n+\t\t\t\t   int argc, const char **argv,\n+\t\t\t\t   const char *prefix)\n+{\n+\tmemset(state, 0, sizeof(*state));\n+\tstate->argc = argc;\n+\tstate->argv = argv;\n+\tstate->prefix = prefix;\n+}\n+\n+static void parse_argv(struct fast_import_state *state);\n+static void parse_get_mark(struct fast_import_state *state, const char *p);\n+static void parse_cat_blob(struct fast_import_state *state, const char *p);\n+static void parse_ls(struct fast_import_state *state,\n+\t\t     const char *p,\n+\t\t     struct branch *b);\n \n static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p)\n {\n@@ -1845,7 +1860,7 @@ static void read_marks(void)\n }\n \n \n-static int read_next_command(void)\n+static int read_next_command(struct fast_import_state *state)\n {\n \tstatic int stdin_eof = 0;\n \n@@ -1867,7 +1882,7 @@ static int read_next_command(void)\n \t\t\tif (!seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n-\t\t\t\tparse_argv();\n+\t\t\t\tparse_argv(state);\n \t\t\t}\n \n \t\t\trc = rc_free;\n@@ -1899,22 +1914,22 @@ static void skip_optional_lf(void)\n \t\tungetc(term_char, stdin);\n }\n \n-static void parse_mark(void)\n+static void parse_mark(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"mark :\", &v)) {\n \t\tnext_mark = strtoumax(v, NULL, 10);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \telse\n \t\tnext_mark = 0;\n }\n \n-static void parse_original_identifier(void)\n+static void parse_original_identifier(struct fast_import_state *state)\n {\n \tconst char *v;\n \tif (skip_prefix(command_buf.buf, \"original-oid \", &v))\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n }\n \n static int parse_data(struct strbuf *sb, uintmax_t limit, uintmax_t *len_res)\n@@ -2068,11 +2083,11 @@ static void parse_and_store_blob(\n \t}\n }\n \n-static void parse_new_blob(void)\n+static void parse_new_blob(struct fast_import_state *state)\n {\n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tparse_and_store_blob(&last_blob, NULL, next_mark);\n }\n \n@@ -2368,7 +2383,9 @@ static void parse_path_space(struct strbuf *sb, const char *p,\n \t(*endp)++;\n }\n \n-static void file_change_m(const char *p, struct branch *b)\n+static void file_change_m(struct fast_import_state *state,\n+\t\t\t  const char *p,\n+\t\t\t  struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct object_entry *oe;\n@@ -2435,10 +2452,10 @@ static void file_change_m(const char *p, struct branch *b)\n \t\tif (S_ISDIR(mode))\n \t\t\tdie(_(\"directories cannot be specified 'inline': %s\"),\n \t\t\t\tcommand_buf.buf);\n-\t\twhile (read_next_command() != EOF) {\n+\t\twhile (read_next_command(state) != EOF) {\n \t\t\tconst char *v;\n \t\t\tif (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\t\tparse_cat_blob(v);\n+\t\t\t\tparse_cat_blob(state, v);\n \t\t\telse {\n \t\t\t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t\t\t\tbreak;\n@@ -2512,7 +2529,10 @@ static void file_change_cr(const char *p, struct branch *b, int rename)\n \t\tleaf.tree);\n }\n \n-static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout)\n+static void note_change_n(struct fast_import_state *state,\n+\t\t\t  const char *p,\n+\t\t\t  struct branch *b,\n+\t\t\t  unsigned char *old_fanout)\n {\n \tstruct object_entry *oe;\n \tstruct branch *s;\n@@ -2577,7 +2597,7 @@ static void note_change_n(const char *p, struct branch *b, unsigned char *old_fa\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), p);\n \n \tif (inline_data) {\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t\tparse_and_store_blob(&last_blob, &oid, 0);\n \t} else if (oe) {\n \t\tif (oe->type != OBJ_BLOB)\n@@ -2644,7 +2664,9 @@ static void parse_from_existing(struct branch *b)\n \t}\n }\n \n-static int parse_objectish(struct branch *b, const char *objectish)\n+static int parse_objectish(struct fast_import_state *state,\n+\t\t\t   struct branch *b,\n+\t\t\t   const char *objectish)\n {\n \tstruct branch *s;\n \tstruct object_id oid;\n@@ -2687,31 +2709,34 @@ static int parse_objectish(struct branch *b, const char *objectish)\n \t\tb->branch_tree.tree = NULL;\n \t}\n \n-\tread_next_command();\n+\tread_next_command(state);\n \treturn 1;\n }\n \n-static int parse_from(struct branch *b)\n+static int parse_from(struct fast_import_state *state, struct branch *b)\n {\n \tconst char *from;\n \n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, from);\n+\treturn parse_objectish(state, b, from);\n }\n \n-static int parse_objectish_with_prefix(struct branch *b, const char *prefix)\n+static int parse_objectish_with_prefix(struct fast_import_state *state,\n+\t\t\t\t       struct branch *b,\n+\t\t\t\t       const char *prefix)\n {\n \tconst char *base;\n \n \tif (!skip_prefix(command_buf.buf, prefix, &base))\n \t\treturn 0;\n \n-\treturn parse_objectish(b, base);\n+\treturn parse_objectish(state, b, base);\n }\n \n-static struct hash_list *parse_merge(unsigned int *count)\n+static struct hash_list *parse_merge(struct fast_import_state *state,\n+\t\t\t\t     unsigned int *count)\n {\n \tstruct hash_list *list = NULL, **tail = &list, *n;\n \tconst char *from;\n@@ -2745,7 +2770,7 @@ static struct hash_list *parse_merge(unsigned int *count)\n \t\ttail = &n->next;\n \n \t\t(*count)++;\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \treturn list;\n }\n@@ -2756,7 +2781,9 @@ struct signature_data {\n \tstruct strbuf data;   /* The actual signature data */\n };\n \n-static void parse_one_signature(struct signature_data *sig, const char *v)\n+static void parse_one_signature(struct fast_import_state *state,\n+\t\t\t\tstruct signature_data *sig,\n+\t\t\t\tconst char *v)\n {\n \tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n \tchar *space = strchr(args, ' ');\n@@ -2781,15 +2808,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v)\n \t\twarning(_(\"'unknown' signature format in gpgsig\"));\n \n \t/* Read signature data */\n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&sig->data, 0, NULL);\n }\n \n-static void discard_one_signature(void)\n+static void discard_one_signature(struct fast_import_state *state)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \n-\tread_next_command();\n+\tread_next_command(state);\n \tparse_data(&data, 0, NULL);\n \tstrbuf_release(&data);\n }\n@@ -2827,13 +2854,14 @@ static void store_signature(struct signature_data *stored_sig,\n \t}\n }\n \n-static void import_one_signature(struct signature_data *sig_sha1,\n+static void import_one_signature(struct fast_import_state *state,\n+\t\t\t\t struct signature_data *sig_sha1,\n \t\t\t\t struct signature_data *sig_sha256,\n \t\t\t\t const char *v)\n {\n \tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n \n-\tparse_one_signature(&sig, v);\n+\tparse_one_signature(state, &sig, v);\n \n \tif (!strcmp(sig.hash_algo, \"sha1\"))\n \t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n@@ -2947,7 +2975,7 @@ static void handle_signature_if_invalid(struct strbuf *new_data,\n \tstrbuf_release(&tmp_buf);\n }\n \n-static void parse_new_commit(const char *arg)\n+static void parse_new_commit(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n@@ -2965,16 +2993,16 @@ static void parse_new_commit(const char *arg)\n \tif (!b)\n \t\tb = new_branch(arg);\n \n-\tread_next_command();\n-\tparse_mark();\n-\tparse_original_identifier();\n+\tread_next_command(state);\n+\tparse_mark(state);\n+\tparse_original_identifier(state);\n \tif (skip_prefix(command_buf.buf, \"author \", &v)) {\n \t\tauthor = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (skip_prefix(command_buf.buf, \"committer \", &v)) {\n \t\tcommitter = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tif (!committer)\n \t\tdie(_(\"expected committer but didn't get one\"));\n@@ -2990,7 +3018,7 @@ static void parse_new_commit(const char *arg)\n \t\t\twarning(_(\"stripping a commit signature\"));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n-\t\t\tdiscard_one_signature();\n+\t\t\tdiscard_one_signature(state);\n \t\t\tbreak;\n \n \t\t/* Second, modes that parse the signature */\n@@ -3001,24 +3029,24 @@ static void parse_new_commit(const char *arg)\n \t\tcase SIGN_STRIP_IF_INVALID:\n \t\tcase SIGN_SIGN_IF_INVALID:\n \t\tcase SIGN_ABORT_IF_INVALID:\n-\t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n+\t\t\timport_one_signature(state, &sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n \t\t/* Third, BUG */\n \t\tdefault:\n \t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n \t\t}\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t}\n \tparse_data(&msg, 0, NULL);\n-\tread_next_command();\n-\tparse_from(b);\n-\tmerge_list = parse_merge(&merge_count);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n+\tmerge_list = parse_merge(state, &merge_count);\n \n \t/* ensure the branch is active/loaded */\n \tif (!b->branch_tree.tree || !max_active_branches) {\n@@ -3031,7 +3059,7 @@ static void parse_new_commit(const char *arg)\n \t/* file_change* */\n \twhile (command_buf.len > 0) {\n \t\tif (skip_prefix(command_buf.buf, \"M \", &v))\n-\t\t\tfile_change_m(v, b);\n+\t\t\tfile_change_m(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"D \", &v))\n \t\t\tfile_change_d(v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"R \", &v))\n@@ -3039,18 +3067,18 @@ static void parse_new_commit(const char *arg)\n \t\telse if (skip_prefix(command_buf.buf, \"C \", &v))\n \t\t\tfile_change_cr(v, b, 0);\n \t\telse if (skip_prefix(command_buf.buf, \"N \", &v))\n-\t\t\tnote_change_n(v, b, &prev_fanout);\n+\t\t\tnote_change_n(state, v, b, &prev_fanout);\n \t\telse if (!strcmp(\"deleteall\", command_buf.buf))\n \t\t\tfile_change_deleteall(b);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, b);\n+\t\t\tparse_ls(state, v, b);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(state, v);\n \t\telse {\n \t\t\tunread_command_buf = 1;\n \t\t\tbreak;\n \t\t}\n-\t\tif (read_next_command() == EOF)\n+\t\tif (read_next_command(state) == EOF)\n \t\t\tbreak;\n \t}\n \n@@ -3188,7 +3216,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \t}\n }\n \n-static void parse_new_tag(const char *arg)\n+static void parse_new_tag(struct fast_import_state *state, const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n \tconst char *from;\n@@ -3207,8 +3235,8 @@ static void parse_new_tag(const char *arg)\n \telse\n \t\tfirst_tag = t;\n \tlast_tag = t;\n-\tread_next_command();\n-\tparse_mark();\n+\tread_next_command(state);\n+\tparse_mark(state);\n \n \t/* from ... */\n \tif (!skip_prefix(command_buf.buf, \"from \", &from))\n@@ -3236,15 +3264,15 @@ static void parse_new_tag(const char *arg)\n \t\t\ttype = oe->type;\n \t} else\n \t\tdie(_(\"invalid ref name or SHA1 expression: %s\"), from);\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* original-oid ... */\n-\tparse_original_identifier();\n+\tparse_original_identifier(state);\n \n \t/* tagger ... */\n \tif (skip_prefix(command_buf.buf, \"tagger \", &v)) {\n \t\ttagger = parse_ident(v);\n-\t\tread_next_command();\n+\t\tread_next_command(state);\n \t} else\n \t\ttagger = NULL;\n \n@@ -3275,7 +3303,7 @@ static void parse_new_tag(const char *arg)\n \t\tt->pack_id = pack_id;\n }\n \n-static void parse_reset_branch(const char *arg)\n+static void parse_reset_branch(struct fast_import_state *state, const char *arg)\n {\n \tstruct branch *b;\n \tconst char *tag_name;\n@@ -3292,8 +3320,8 @@ static void parse_reset_branch(const char *arg)\n \t}\n \telse\n \t\tb = new_branch(arg);\n-\tread_next_command();\n-\tparse_from(b);\n+\tread_next_command(state);\n+\tparse_from(state, b);\n \tif (b->delete && skip_prefix(b->name, \"refs/tags/\", &tag_name)) {\n \t\t/*\n \t\t * Elsewhere, we call dump_branches() before dump_tags(),\n@@ -3378,7 +3406,8 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid)\n \t\tfree(buf);\n }\n \n-static void parse_get_mark(const char *p)\n+static void parse_get_mark(struct fast_import_state *state UNUSED,\n+\t\t\t   const char *p)\n {\n \tstruct object_entry *oe;\n \tchar output[GIT_MAX_HEXSZ + 2];\n@@ -3395,7 +3424,8 @@ static void parse_get_mark(const char *p)\n \tcat_blob_write(output, the_hash_algo->hexsz + 1);\n }\n \n-static void parse_cat_blob(const char *p)\n+static void parse_cat_blob(struct fast_import_state *state UNUSED,\n+\t\t\t   const char *p)\n {\n \tstruct object_entry *oe;\n \tstruct object_id oid;\n@@ -3560,7 +3590,9 @@ static void print_ls(int mode, const unsigned char *hash, const char *path)\n \tcat_blob_write(line.buf, line.len);\n }\n \n-static void parse_ls(const char *p, struct branch *b)\n+static void parse_ls(struct fast_import_state *state UNUSED,\n+\t\t     const char *p,\n+\t\t     struct branch *b)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \tstruct tree_entry *root = NULL;\n@@ -3607,13 +3639,13 @@ static void checkpoint(void)\n \tdump_marks();\n }\n \n-static void parse_checkpoint(void)\n+static void parse_checkpoint(struct fast_import_state *state UNUSED)\n {\n \tcheckpoint_requested = 1;\n \tskip_optional_lf();\n }\n \n-static void parse_progress(void)\n+static void parse_progress(struct fast_import_state *state UNUSED)\n {\n \tfwrite(command_buf.buf, 1, command_buf.len, stdout);\n \tfputc('\\n', stdout);\n@@ -3621,37 +3653,40 @@ static void parse_progress(void)\n \tskip_optional_lf();\n }\n \n-static void parse_alias(void)\n+static void parse_alias(struct fast_import_state *state)\n {\n \tstruct object_entry *e;\n \tstruct branch b;\n \n \tskip_optional_lf();\n-\tread_next_command();\n+\tread_next_command(state);\n \n \t/* mark ... */\n-\tparse_mark();\n+\tparse_mark(state);\n \tif (!next_mark)\n \t\tdie(_(\"expected 'mark' command, got %s\"), command_buf.buf);\n \n \t/* to ... */\n \tmemset(&b, 0, sizeof(b));\n-\tif (!parse_objectish_with_prefix(&b, \"to \"))\n+\tif (!parse_objectish_with_prefix(state, &b, \"to \"))\n \t\tdie(_(\"expected 'to' command, got %s\"), command_buf.buf);\n \te = find_object(&b.oid);\n \tassert(e);\n \tinsert_mark(&marks, next_mark, e);\n }\n \n-static char* make_fast_import_path(const char *path)\n+static char* make_fast_import_path(struct fast_import_state *state,\n+\t\t\t\t   const char *path)\n {\n \tif (!relative_marks_paths || is_absolute_path(path))\n-\t\treturn prefix_filename(global_prefix, path);\n+\t\treturn prefix_filename(state->prefix, path);\n \treturn repo_git_path(the_repository, \"info/fast-import/%s\", path);\n }\n \n-static void option_import_marks(const char *marks,\n-\t\t\t\t\tint from_stream, int ignore_missing)\n+static void option_import_marks(struct fast_import_state *state,\n+\t\t\t\tconst char *marks,\n+\t\t\t\tint from_stream,\n+\t\t\t\tint ignore_missing)\n {\n \tif (import_marks_file) {\n \t\tif (from_stream)\n@@ -3663,7 +3698,7 @@ static void option_import_marks(const char *marks,\n \t}\n \n \tfree(import_marks_file);\n-\timport_marks_file = make_fast_import_path(marks);\n+\timport_marks_file = make_fast_import_path(state, marks);\n \timport_marks_file_from_stream = from_stream;\n \timport_marks_file_ignore_missing = ignore_missing;\n }\n@@ -3703,13 +3738,15 @@ static void option_active_branches(const char *branches)\n \tmax_active_branches = ulong_arg(\"--active-branches\", branches);\n }\n \n-static void option_export_marks(const char *marks)\n+static void option_export_marks(struct fast_import_state *state,\n+\t\t\t\tconst char *marks)\n {\n \tfree(export_marks_file);\n-\texport_marks_file = make_fast_import_path(marks);\n+\texport_marks_file = make_fast_import_path(state, marks);\n }\n \n-static void option_cat_blob_fd(const char *fd)\n+static void option_cat_blob_fd(struct fast_import_state *state UNUSED,\n+\t\t\t       const char *fd)\n {\n \tunsigned long n = ulong_arg(\"--cat-blob-fd\", fd);\n \tif (n > (unsigned long) INT_MAX)\n@@ -3717,16 +3754,19 @@ static void option_cat_blob_fd(const char *fd)\n \tcat_blob_fd = (int) n;\n }\n \n-static void option_export_pack_edges(const char *edges)\n+static void option_export_pack_edges(struct fast_import_state *state,\n+\t\t\t\t     const char *edges)\n {\n-\tchar *fn = prefix_filename(global_prefix, edges);\n+\tchar *fn = prefix_filename(state->prefix, edges);\n \tif (pack_edges)\n \t\tfclose(pack_edges);\n \tpack_edges = xfopen(fn, \"a\");\n \tfree(fn);\n }\n \n-static void option_rewrite_submodules(const char *arg, struct string_list *list)\n+static void option_rewrite_submodules(struct fast_import_state *state,\n+\t\t\t\t      const char *arg,\n+\t\t\t\t      struct string_list *list)\n {\n \tstruct mark_set *ms;\n \tFILE *fp;\n@@ -3738,7 +3778,7 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list)\n \tf++;\n \tCALLOC_ARRAY(ms, 1);\n \n-\tf = prefix_filename(global_prefix, f);\n+\tf = prefix_filename(state->prefix, f);\n \tfp = fopen(f, \"r\");\n \tif (!fp)\n \t\tdie_errno(_(\"cannot read '%s'\"), f);\n@@ -3794,7 +3834,7 @@ static void option_quiet(void)\n \tquiet = 1;\n }\n \n-static int parse_one_option(const char *option)\n+static int parse_one_option(struct fast_import_state *state, const char *option)\n {\n \tif (skip_prefix(option, \"max-pack-size=\", &option)) {\n \t\toption_max_pack_size(option);\n@@ -3805,7 +3845,7 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"active-branches=\", &option)) {\n \t\toption_active_branches(option);\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n-\t\toption_export_pack_edges(option);\n+\t\toption_export_pack_edges(state, option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n \t\toption_signed_commits(option);\n \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n@@ -3823,34 +3863,38 @@ static int parse_one_option(const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(const char *feature, int from_stream)\n+static void check_unsafe_feature(struct fast_import_state *state UNUSED,\n+\t\t\t\t const char *feature,\n+\t\t\t\t int from_stream)\n {\n \tif (from_stream && !allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n \n-static int parse_one_feature(const char *feature, int from_stream)\n+static int parse_one_feature(struct fast_import_state *state,\n+\t\t\t     const char *feature,\n+\t\t\t     int from_stream)\n {\n \tconst char *arg;\n \n \tif (skip_prefix(feature, \"date-format=\", &arg)) {\n \t\toption_date_format(arg);\n \t} else if (skip_prefix(feature, \"import-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 0);\n+\t\tcheck_unsafe_feature(state, \"import-marks\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 0);\n \t} else if (skip_prefix(feature, \"import-marks-if-exists=\", &arg)) {\n-\t\tcheck_unsafe_feature(\"import-marks-if-exists\", from_stream);\n-\t\toption_import_marks(arg, from_stream, 1);\n+\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\", from_stream);\n+\t\toption_import_marks(state, arg, from_stream, 1);\n \t} else if (skip_prefix(feature, \"export-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(feature, from_stream);\n-\t\toption_export_marks(arg);\n+\t\tcheck_unsafe_feature(state, feature, from_stream);\n+\t\toption_export_marks(state, arg);\n \t} else if (!strcmp(feature, \"alias\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (skip_prefix(feature, \"rewrite-submodules-to=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_to);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_to);\n \t} else if (skip_prefix(feature, \"rewrite-submodules-from=\", &arg)) {\n-\t\toption_rewrite_submodules(arg, &sub_marks_from);\n+\t\toption_rewrite_submodules(state, arg, &sub_marks_from);\n \t} else if (!strcmp(feature, \"get-mark\")) {\n \t\t; /* Don't die - this feature is supported */\n \t} else if (!strcmp(feature, \"cat-blob\")) {\n@@ -3872,23 +3916,23 @@ static int parse_one_feature(const char *feature, int from_stream)\n \treturn 1;\n }\n \n-static void parse_feature(const char *feature)\n+static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n-\tif (parse_one_feature(feature, 1))\n+\tif (parse_one_feature(state, feature, 1))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support feature %s.\"), feature);\n }\n \n-static void parse_option(const char *option)\n+static void parse_option(struct fast_import_state *state, const char *option)\n {\n \tif (seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n-\tif (parse_one_option(option))\n+\tif (parse_one_option(state, option))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support option: %s\"), option);\n@@ -3924,12 +3968,12 @@ static void git_pack_config(void)\n static const char fast_import_usage[] =\n \"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n \n-static void parse_argv(void)\n+static void parse_argv(struct fast_import_state *state)\n {\n \tunsigned int i;\n \n-\tfor (i = 1; i < global_argc; i++) {\n-\t\tconst char *a = global_argv[i];\n+\tfor (i = 1; i < state->argc; i++) {\n+\t\tconst char *a = state->argv[i];\n \n \t\tif (*a != '-' || !strcmp(a, \"--\"))\n \t\t\tbreak;\n@@ -3937,20 +3981,20 @@ static void parse_argv(void)\n \t\tif (!skip_prefix(a, \"--\", &a))\n \t\t\tdie(_(\"unknown option %s\"), a);\n \n-\t\tif (parse_one_option(a))\n+\t\tif (parse_one_option(state, a))\n \t\t\tcontinue;\n \n-\t\tif (parse_one_feature(a, 0))\n+\t\tif (parse_one_feature(state, a, 0))\n \t\t\tcontinue;\n \n \t\tif (skip_prefix(a, \"cat-blob-fd=\", &a)) {\n-\t\t\toption_cat_blob_fd(a);\n+\t\t\toption_cat_blob_fd(state, a);\n \t\t\tcontinue;\n \t\t}\n \n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n-\tif (i != global_argc)\n+\tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n \tseen_data_command = 1;\n@@ -3964,6 +4008,8 @@ int cmd_fast_import(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo)\n {\n+\tstruct fast_import_state state;\n+\n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n \treset_pack_idx_option(&pack_idx_opts);\n@@ -3992,9 +4038,7 @@ int cmd_fast_import(int argc,\n \t\t\tallow_unsafe_features = 1;\n \t}\n \n-\tglobal_argc = argc;\n-\tglobal_argv = argv;\n-\tglobal_prefix = prefix;\n+\tfast_import_state_init(&state, argc, argv, prefix);\n \n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n@@ -4004,34 +4048,34 @@ int cmd_fast_import(int argc,\n \tstart_packfile();\n \tset_die_routine(die_nicely);\n \tset_checkpoint_signal();\n-\twhile (read_next_command() != EOF) {\n+\twhile (read_next_command(&state) != EOF) {\n \t\tconst char *v;\n \t\tif (!strcmp(\"blob\", command_buf.buf))\n-\t\t\tparse_new_blob();\n+\t\t\tparse_new_blob(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"commit \", &v))\n-\t\t\tparse_new_commit(v);\n+\t\t\tparse_new_commit(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"tag \", &v))\n-\t\t\tparse_new_tag(v);\n+\t\t\tparse_new_tag(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"reset \", &v))\n-\t\t\tparse_reset_branch(v);\n+\t\t\tparse_reset_branch(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"ls \", &v))\n-\t\t\tparse_ls(v, NULL);\n+\t\t\tparse_ls(&state, v, NULL);\n \t\telse if (skip_prefix(command_buf.buf, \"cat-blob \", &v))\n-\t\t\tparse_cat_blob(v);\n+\t\t\tparse_cat_blob(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"get-mark \", &v))\n-\t\t\tparse_get_mark(v);\n+\t\t\tparse_get_mark(&state, v);\n \t\telse if (!strcmp(\"checkpoint\", command_buf.buf))\n-\t\t\tparse_checkpoint();\n+\t\t\tparse_checkpoint(&state);\n \t\telse if (!strcmp(\"done\", command_buf.buf))\n \t\t\tbreak;\n \t\telse if (!strcmp(\"alias\", command_buf.buf))\n-\t\t\tparse_alias();\n+\t\t\tparse_alias(&state);\n \t\telse if (starts_with(command_buf.buf, \"progress \"))\n-\t\t\tparse_progress();\n+\t\t\tparse_progress(&state);\n \t\telse if (skip_prefix(command_buf.buf, \"feature \", &v))\n-\t\t\tparse_feature(v);\n+\t\t\tparse_feature(&state, v);\n \t\telse if (skip_prefix(command_buf.buf, \"option git \", &v))\n-\t\t\tparse_option(v);\n+\t\t\tparse_option(&state, v);\n \t\telse if (starts_with(command_buf.buf, \"option \"))\n \t\t\t/* ignore non-git options*/;\n \t\telse\n@@ -4043,7 +4087,7 @@ int cmd_fast_import(int argc,\n \n \t/* argv hasn't been parsed yet, do so */\n \tif (!seen_data_command)\n-\t\tparse_argv();\n+\t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n \t\tdie(_(\"stream ends early\"));\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550256","messageId":"20260811083314.2023489-9-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 08/12] fast-import: move command state globals into 'struct fast_import_state'","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:10Z","receivedAt":"2026-08-11T08:33:47Z","isPatch":true,"body":"A previous commit introduced 'struct fast_import_state' to hold some\ncommand state, and reduce the need for global variables.\n\nLet's continue in the same direction and move two more global variables\nthat describe the command state into it: 'seen_data_command' and\n'allow_unsafe_features'.\n\nAll the sites accessing these variables are already in functions that\nreceive the 'state' parameter (or in cmd_fast_import() which owns the\nstruct), so no additional threading is needed.\n\nAs 'state->allow_unsafe_features' is now dereferenced in\ncheck_unsafe_feature(), its 'state' parameter is no longer unused, so\nthe UNUSED marker is removed.\n\nThe fast_import_state_init() call is moved up before the early\ncommand-line scan for '--allow-unsafe-features', so that this option\ncan be recorded directly into the struct without being clobbered by\nthe memset() in fast_import_state_init().\n\nThis is a mechanical refactoring with no intended behavior change.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 24 ++++++++++++------------\n 1 file changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 0f838d8488..52da29c1bd 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -257,9 +257,7 @@ static struct recent_command *rc_free;\n static unsigned int cmd_save = 100;\n static uintmax_t next_mark;\n static struct strbuf new_data = STRBUF_INIT;\n-static int seen_data_command;\n static int require_explicit_termination;\n-static int allow_unsafe_features;\n \n /* Signal handling */\n static volatile sig_atomic_t checkpoint_requested;\n@@ -277,6 +275,8 @@ struct fast_import_state {\n \tint argc;\n \tconst char **argv;\n \tconst char *prefix;\n+\tint seen_data_command;\n+\tint allow_unsafe_features;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n@@ -1879,7 +1879,7 @@ static int read_next_command(struct fast_import_state *state)\n \t\t\tif (stdin_eof)\n \t\t\t\treturn EOF;\n \n-\t\t\tif (!seen_data_command\n+\t\t\tif (!state->seen_data_command\n \t\t\t\t&& !starts_with(command_buf.buf, \"feature \")\n \t\t\t\t&& !starts_with(command_buf.buf, \"option \")) {\n \t\t\t\tparse_argv(state);\n@@ -3863,11 +3863,11 @@ static int parse_one_option(struct fast_import_state *state, const char *option)\n \treturn 1;\n }\n \n-static void check_unsafe_feature(struct fast_import_state *state UNUSED,\n+static void check_unsafe_feature(struct fast_import_state *state,\n \t\t\t\t const char *feature,\n \t\t\t\t int from_stream)\n {\n-\tif (from_stream && !allow_unsafe_features)\n+\tif (from_stream && !state->allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n@@ -3918,7 +3918,7 @@ static int parse_one_feature(struct fast_import_state *state,\n \n static void parse_feature(struct fast_import_state *state, const char *feature)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n \tif (parse_one_feature(state, feature, 1))\n@@ -3929,7 +3929,7 @@ static void parse_feature(struct fast_import_state *state, const char *feature)\n \n static void parse_option(struct fast_import_state *state, const char *option)\n {\n-\tif (seen_data_command)\n+\tif (state->seen_data_command)\n \t\tdie(_(\"got option command '%s' after data command\"), option);\n \n \tif (parse_one_option(state, option))\n@@ -3997,7 +3997,7 @@ static void parse_argv(struct fast_import_state *state)\n \tif (i != state->argc)\n \t\tusage(fast_import_usage);\n \n-\tseen_data_command = 1;\n+\tstate->seen_data_command = 1;\n \tif (import_marks_file)\n \t\tread_marks();\n \tbuild_mark_map(&sub_marks_from, &sub_marks_to);\n@@ -4012,6 +4012,8 @@ int cmd_fast_import(int argc,\n \n \tshow_usage_if_asked(argc, argv, fast_import_usage);\n \n+\tfast_import_state_init(&state, argc, argv, prefix);\n+\n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\n \n@@ -4035,11 +4037,9 @@ int cmd_fast_import(int argc,\n \t\tif (*arg != '-' || !strcmp(arg, \"--\"))\n \t\t\tbreak;\n \t\tif (!strcmp(arg, \"--allow-unsafe-features\"))\n-\t\t\tallow_unsafe_features = 1;\n+\t\t\tstate.allow_unsafe_features = 1;\n \t}\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n-\n \trc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free));\n \tfor (unsigned int i = 0; i < (cmd_save - 1); i++)\n \t\trc_free[i].next = &rc_free[i + 1];\n@@ -4086,7 +4086,7 @@ int cmd_fast_import(int argc,\n \t}\n \n \t/* argv hasn't been parsed yet, do so */\n-\tif (!seen_data_command)\n+\tif (!state.seen_data_command)\n \t\tparse_argv(&state);\n \n \tif (require_explicit_termination && feof(stdin))\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550257","messageId":"20260811083314.2023489-10-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 09/12] fast-import: use struct option for usage string","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:11Z","receivedAt":"2026-08-11T08:33:50Z","isPatch":true,"body":"Currently `git fast-import -h` shows the following on a single line:\n\n  usage : git fast-import [--date-format=<f>] [--max-pack-size=<n>] \\\n                          [--big-file-threshold=<n>] [--depth=<n>] \\\n                          [--active-branches=<n>] \\\n                          [--export-marks=<marks.file>]\n\nThis output has a number of issues like:\n\n  - It's missing a lot of options.\n  - It's not consistent with the SYNOPSIS section of the doc.\n  - With `--help-all` instead of `-h` additional hidden options should\n    be shown, but that's not the case.\n  - It's not standard style anymore.\n  - Most other Git commands show additional lines for most of the\n    options they support.\n\nAlso while most commands use the parse-options API to handle their\noptions, \"builtin/fast-import.c\" still doesn't use it.\n\nLet's improve on that by using the parse-options API to display the\noptions when `-h` and `--help-all` are used.\n\nWhile at it, let's make the SYNOPSIS section of\n\"Documentation/git-fast-import.adoc\" consistent with the new usage\nstring.\n\nThis deliberately leaves it to future work to also use the\nparse-options API to actually parse the options.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n Documentation/git-fast-import.adoc |  2 +-\n builtin/fast-import.c              | 83 +++++++++++++++++++++++++++---\n t/t0450/adoc-help-mismatches       |  1 -\n 3 files changed, 78 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex d68bc52b7e..7c5900e048 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -9,7 +9,7 @@ git-fast-import - Backend for fast Git data importers\n SYNOPSIS\n --------\n [verse]\n-frontend | 'git fast-import' [<options>]\n+'git fast-import' [<options>]\n \n DESCRIPTION\n -----------\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 52da29c1bd..879c286043 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -30,6 +30,7 @@\n #include \"khash.h\"\n #include \"date.h\"\n #include \"gpg-interface.h\"\n+#include \"parse-options.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -277,16 +278,18 @@ struct fast_import_state {\n \tconst char *prefix;\n \tint seen_data_command;\n \tint allow_unsafe_features;\n+\tstruct option *option;\n };\n \n static void fast_import_state_init(struct fast_import_state *state,\n \t\t\t\t   int argc, const char **argv,\n-\t\t\t\t   const char *prefix)\n+\t\t\t\t   const char *prefix, struct option *option)\n {\n \tmemset(state, 0, sizeof(*state));\n \tstate->argc = argc;\n \tstate->argv = argv;\n \tstate->prefix = prefix;\n+\tstate->option = option;\n }\n \n static void parse_argv(struct fast_import_state *state);\n@@ -3965,8 +3968,10 @@ static void git_pack_config(void)\n \trepo_config(the_repository, git_default_config, NULL);\n }\n \n-static const char fast_import_usage[] =\n-\"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]\";\n+static const char *const fast_import_usage[] = {\n+\tN_(\"git fast-import [<options>]\"),\n+\tNULL\n+};\n \n static void parse_argv(struct fast_import_state *state)\n {\n@@ -3995,7 +4000,7 @@ static void parse_argv(struct fast_import_state *state)\n \t\tdie(_(\"unknown option --%s\"), a);\n \t}\n \tif (i != state->argc)\n-\t\tusage(fast_import_usage);\n+\t\tusage_with_options(fast_import_usage, state->option);\n \n \tstate->seen_data_command = 1;\n \tif (import_marks_file)\n@@ -4010,9 +4015,75 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\tshow_usage_if_asked(argc, argv, fast_import_usage);\n+\tunsigned long pack_size_limit, big_file_threshold;\n+\tchar *edges, *signed_commits, *signed_tags, *date_format;\n+\tchar *import_marks_if_exists, *submodules_from, *submodules_to;\n \n-\tfast_import_state_init(&state, argc, argv, prefix);\n+\t/*\n+\t * NEEDSWORK: For now this is used only to render\n+\t * `-h`/`--help-all` usage messages. The actual parsing is\n+\t * done by parse_one_option()/parse_one_feature().\n+\t */\n+\tstruct option fast_import_options[] = {\n+\t\tOPT_GROUP(N_(\"Common\")),\n+\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n+\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"stats\", &show_stats,\n+\t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n+\t\t\t   PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"quiet\", &quiet,\n+\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"force\", &force_update,\n+\t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL_F(0, \"done\", &require_explicit_termination,\n+\t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n+\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n+\t\t\t     N_(\"maximum size of each output pack file\")),\n+\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n+\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n+\t\tOPT_UNSIGNED(0, \"depth\", &max_depth,\n+\t\t\t     N_(\"maximum delta depth\")),\n+\t\tOPT_UNSIGNED(0, \"active-branches\", &max_active_branches,\n+\t\t\t     N_(\"maximum number of branches to maintain active\")),\n+\t\tOPT_GROUP(N_(\"Marks\")),\n+\t\tOPT_STRING_F(0, \"import-marks\", &import_marks_file, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n+\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-marks\", &export_marks_file, N_(\"file\"),\n+\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_BOOL(0, \"relative-marks\", &relative_marks_paths,\n+\t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n+\t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"rewrite-submodules-to\", &submodules_to, N_(\"name:filename\"),\n+\t\t\t     N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_GROUP(N_(\"Signing\")),\n+\t\tOPT_STRING_F(0, \"signed-commits\", &signed_commits, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed commits\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"signed-tags\", &signed_tags, N_(\"mode\"),\n+\t\t\t     N_(\"how to handle signed tags\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n+\t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &state.allow_unsafe_features,\n+\t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n+\t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n+\t\t\t     N_(\"dump edge commits to <file>\"),\n+\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob_fd,\n+\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n+\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_END()\n+\t};\n+\n+\tshow_usage_with_options_if_asked(argc, argv, fast_import_usage, fast_import_options);\n+\n+\tfast_import_state_init(&state, argc, argv, prefix, fast_import_options);\n \n \treset_pack_idx_option(&pack_idx_opts);\n \tgit_pack_config();\ndiff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches\nindex c4a55ff4e3..baf3b1d809 100644\n--- a/t/t0450/adoc-help-mismatches\n+++ b/t/t0450/adoc-help-mismatches\n@@ -12,7 +12,6 @@ column\n credential\n credential-cache\n credential-store\n-fast-import\n fetch-pack\n fmt-merge-msg\n format-patch\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550258","messageId":"20260811083314.2023489-11-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 10/12] fast-import: use callbacks to parse some options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:12Z","receivedAt":"2026-08-11T08:33:52Z","isPatch":true,"body":"A previous commit started using the parse-option API to generate proper\n`git fast-import -h` and `git fast-import --help-all` output.\n\nLet's prepare for when we can use that API to also parse the options by\nusing OPT_CALLBACK for some options that require special processing of\ntheir arguments.\n\nA following commit will actually parse the options using these\ncallbacks.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 208 ++++++++++++++++++++++++++++++++++--------\n 1 file changed, 168 insertions(+), 40 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 879c286043..40cc9c4a23 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -4008,6 +4008,126 @@ static void parse_argv(struct fast_import_state *state)\n \tbuild_mark_map(&sub_marks_from, &sub_marks_to);\n }\n \n+static int option_parse_date_format(const struct option *opt UNUSED,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_date_format(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_export_pack_edges(const struct option *opt,\n+\t\t\t\t\t  const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_export_pack_edges(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_max_pack_size(const struct option *opt UNUSED,\n+\t\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_max_pack_size(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_big_file_threshold(const struct option *opt UNUSED,\n+\t\t\t\t\t   const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_big_file_threshold(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_signed_commits(const struct option *opt UNUSED,\n+\t\t\t\t       const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_signed_commits(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_signed_tags(const struct option *opt UNUSED,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_signed_tags(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_rewrite_submodules_from(const struct option *opt,\n+\t\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_rewrite_submodules(opt->value, arg, &sub_marks_from);\n+\treturn 0;\n+}\n+\n+static int option_parse_rewrite_submodules_to(const struct option *opt,\n+\t\t\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_rewrite_submodules(opt->value, arg, &sub_marks_to);\n+\treturn 0;\n+}\n+\n+static int option_parse_cat_blob_fd(const struct option *opt,\n+\t\t\t\t    const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_cat_blob_fd(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_import_marks(const struct option *opt,\n+\t\t\t\t     const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_import_marks(opt->value, arg, 0, 0);\n+\treturn 0;\n+}\n+\n+static int option_parse_import_marks_if_exists(const struct option *opt,\n+\t\t\t\t\t       const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_import_marks(opt->value, arg, 0, 1);\n+\treturn 0;\n+}\n+\n+static int option_parse_export_marks(const struct option *opt,\n+\t\t\t\t     const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_export_marks(opt->value, arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_depth(const struct option *opt UNUSED,\n+\t\t\t      const char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_depth(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_active_branches(const struct option *opt UNUSED,\n+\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_active_branches(arg);\n+\treturn 0;\n+}\n+\n+static int option_parse_quiet(const struct option *opt UNUSED,\n+\t\t\t      const char *arg UNUSED, int unset)\n+{\n+\tBUG_ON_OPT_NEG(unset);\n+\toption_quiet();\n+\treturn 0;\n+}\n+\n int cmd_fast_import(int argc,\n \t\t    const char **argv,\n \t\t    const char *prefix,\n@@ -4015,10 +4135,6 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\tunsigned long pack_size_limit, big_file_threshold;\n-\tchar *edges, *signed_commits, *signed_tags, *date_format;\n-\tchar *import_marks_if_exists, *submodules_from, *submodules_to;\n-\n \t/*\n \t * NEEDSWORK: For now this is used only to render\n \t * `-h`/`--help-all` usage messages. The actual parsing is\n@@ -4026,58 +4142,70 @@ int cmd_fast_import(int argc,\n \t */\n \tstruct option fast_import_options[] = {\n \t\tOPT_GROUP(N_(\"Common\")),\n-\t\tOPT_STRING_F(0, \"date-format\", &date_format, N_(\"fmt\"),\n-\t\t\t   N_(\"format of the commit/tag dates\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"date-format\", NULL, N_(\"fmt\"),\n+\t\t\t       N_(\"format of the commit/tag dates\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_date_format),\n \t\tOPT_BOOL_F(0, \"stats\", &show_stats,\n \t\t\t   N_(\"display some basic statistics (objects, packfiles and memory)\"),\n \t\t\t   PARSE_OPT_NONEG),\n-\t\tOPT_BOOL_F(0, \"quiet\", &quiet,\n-\t\t\t   N_(\"disable the output shown by --stats\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"quiet\", NULL, NULL,\n+\t\t\t       N_(\"disable the output shown by --stats\"),\n+\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_quiet),\n \t\tOPT_BOOL_F(0, \"force\", &force_update,\n \t\t\t   N_(\"force updating modified existing branches\"), PARSE_OPT_NONEG),\n \t\tOPT_BOOL_F(0, \"done\", &require_explicit_termination,\n \t\t\t   N_(\"require a terminating 'done' command\"), PARSE_OPT_NONEG),\n-\t\tOPT_UNSIGNED(0, \"max-pack-size\", &pack_size_limit,\n-\t\t\t     N_(\"maximum size of each output pack file\")),\n-\t\tOPT_UNSIGNED(0, \"big-file-threshold\", &big_file_threshold,\n-\t\t\t     N_(\"maximum size of a blob that will be deltified\")),\n-\t\tOPT_UNSIGNED(0, \"depth\", &max_depth,\n-\t\t\t     N_(\"maximum delta depth\")),\n-\t\tOPT_UNSIGNED(0, \"active-branches\", &max_active_branches,\n-\t\t\t     N_(\"maximum number of branches to maintain active\")),\n+\t\tOPT_CALLBACK_F(0, \"max-pack-size\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum size of each output pack file\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_max_pack_size),\n+\t\tOPT_CALLBACK_F(0, \"big-file-threshold\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum size of a blob that will be deltified\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_big_file_threshold),\n+\t\tOPT_CALLBACK_F(0, \"depth\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum delta depth\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_depth),\n+\t\tOPT_CALLBACK_F(0, \"active-branches\", NULL, N_(\"n\"),\n+\t\t\t       N_(\"maximum number of branches to maintain active\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_active_branches),\n \t\tOPT_GROUP(N_(\"Marks\")),\n-\t\tOPT_STRING_F(0, \"import-marks\", &import_marks_file, N_(\"file\"),\n-\t\t\t     N_(\"import marks from <file>\"), PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"import-marks-if-exists\", &import_marks_if_exists, N_(\"file\"),\n-\t\t\t     N_(\"import marks from <file> if it exists\"), PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"export-marks\", &export_marks_file, N_(\"file\"),\n-\t\t\t     N_(\"dump marks to <file>\"), PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"import-marks\", &state, N_(\"file\"),\n+\t\t\t       N_(\"import marks from <file>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_import_marks),\n+\t\tOPT_CALLBACK_F(0, \"import-marks-if-exists\", &state, N_(\"file\"),\n+\t\t\t       N_(\"import marks from <file> if it exists\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_import_marks_if_exists),\n+\t\tOPT_CALLBACK_F(0, \"export-marks\", &state, N_(\"file\"),\n+\t\t\t       N_(\"dump marks to <file>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_export_marks),\n \t\tOPT_BOOL(0, \"relative-marks\", &relative_marks_paths,\n \t\t\t N_(\"are --(import|export)-marks= paths relative to '.git/info/fast-import'?\")),\n \t\tOPT_GROUP(N_(\"Submodule rewrite\")),\n-\t\tOPT_STRING_F(0, \"rewrite-submodules-from\", &submodules_from, N_(\"name:filename\"),\n-\t\t\t     N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n-\t\t\t     PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"rewrite-submodules-to\", &submodules_to, N_(\"name:filename\"),\n-\t\t\t     N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n-\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"rewrite-submodules-from\", &state, N_(\"name:filename\"),\n+\t\t\t       N_(\"rewrite object IDs for submodule <name> from <filename>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_rewrite_submodules_from),\n+\t\tOPT_CALLBACK_F(0, \"rewrite-submodules-to\", &state, N_(\"name:filename\"),\n+\t\t\t       N_(\"rewrite object IDs for submodule <name> to <filename>\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_rewrite_submodules_to),\n \t\tOPT_GROUP(N_(\"Signing\")),\n-\t\tOPT_STRING_F(0, \"signed-commits\", &signed_commits, N_(\"mode\"),\n-\t\t\t     N_(\"how to handle signed commits\"),\n-\t\t\t     PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"signed-tags\", &signed_tags, N_(\"mode\"),\n-\t\t\t     N_(\"how to handle signed tags\"),\n-\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"signed-commits\", NULL, N_(\"mode\"),\n+\t\t\t       N_(\"how to handle signed commits\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_signed_commits),\n+\t\tOPT_CALLBACK_F(0, \"signed-tags\", NULL, N_(\"mode\"),\n+\t\t\t       N_(\"how to handle signed tags\"),\n+\t\t\t       PARSE_OPT_NONEG, option_parse_signed_tags),\n \t\tOPT_HIDDEN_GROUP(N_(\"Advanced\")),\n \t\tOPT_BOOL_F(0, \"allow-unsafe-features\", &state.allow_unsafe_features,\n \t\t\t   N_(\"allow unsafe mark commands from the stream\"),\n \t\t\t   PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n-\t\tOPT_STRING_F(0, \"export-pack-edges\", &edges, N_(\"file\"),\n-\t\t\t     N_(\"dump edge commits to <file>\"),\n-\t\t\t     PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n-\t\tOPT_INTEGER_F(0, \"cat-blob-fd\", &cat_blob_fd,\n-\t\t\t    N_(\"write some responses to <fd> instead of stdout\"),\n-\t\t\t      PARSE_OPT_HIDDEN | PARSE_OPT_NONEG),\n+\t\tOPT_CALLBACK_F(0, \"export-pack-edges\", &state, N_(\"file\"),\n+\t\t\t       N_(\"dump edge commits to <file>\"),\n+\t\t\t       PARSE_OPT_HIDDEN | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_export_pack_edges),\n+\t\tOPT_CALLBACK_F(0, \"cat-blob-fd\", &state, N_(\"fd\"),\n+\t\t\t       N_(\"write some responses to <fd> instead of stdout\"),\n+\t\t\t       PARSE_OPT_HIDDEN | PARSE_OPT_NONEG,\n+\t\t\t       option_parse_cat_blob_fd),\n \t\tOPT_END()\n \t};\n \n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550259","messageId":"20260811083314.2023489-12-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 11/12] fast-import: use parse_options() for command line options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:13Z","receivedAt":"2026-08-11T08:33:55Z","isPatch":true,"body":"Previous commits have started to use the parse-options API to display\noutput from `git fast-import -h` and `git fast-import --help-all` and\nto prepare for parsing the command line options using this API.\n\nLet's now actually use the API to parse command line options.\n\nThis brings a number of changes that are mostly beneficial:\n\n  - The `--alias`, `--get-mark`, `--cat-blob`, `--ls` and `--notes`\n    options are no longer accepted on the command line. They were\n    previously accepted as no-ops because parse_argv() fell through to\n    parse_one_feature(). They are not documented in the OPTIONS section\n    and are only meaningful as in-stream feature assertions, so\n    accepting them on the command line was an accident of code sharing\n    dating back to 9c8398f0c9 (fast-import: add option command,\n    2009-12-04).\n\n  - Abbreviated options like `--dep=5` now work since parse_options()\n    allows unambiguous prefixes.\n\n  - As `--cat-blob` is an abbreviation of `--cat-blob-fd`, using the\n    former on the command line will fail with \"option `cat-blob-fd'\n    requires a value\" unlike the other four options that are not\n    accepted anymore on the command line (see above).\n\n  - Value-taking options now also accept the space-separated\n    `--opt value` form, like `--depth 5`, in addition to the\n    `--opt=value` form.\n\n  - A bare or trailing `--` is now accepted and the stream is read\n    normally, while it used to be a usage error.\n\n  - The error messages for some options might differ a bit.\n\n  - The code is shorter and more standard.\n\nNote that parse_one_feature() is now always called with its\n`from_stream` argument set to 1, but the code simplifications that\ncan be made are left for a following clean-up commit.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n Documentation/git-fast-import.adoc |  7 +++++\n builtin/fast-import.c              | 43 ++++++++++--------------------\n t/t9300-fast-import.sh             |  7 +++++\n 3 files changed, 28 insertions(+), 29 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 7c5900e048..fd165e11d2 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -65,6 +65,13 @@ Only enable this option if you trust the program generating the\n fast-import stream! This option is enabled automatically for\n remote-helpers that use the `import` capability, as they are\n already trusted to run their own code.\n++\n+Note that this option has to be spelled in full, and has to appear\n+before any option whose value is separated from it by a space, for\n+the unsafe `feature` commands in the stream to be allowed. So\n+`--allow-unsafe` or `--depth 5 --allow-unsafe-features` still refuse\n+them, while `--allow-unsafe-features --depth 5` and\n+`--depth=5 --allow-unsafe-features` allow them.\n \n `--signed-tags=<mode>`::\n \tSpecify how to handle signed tags. Behaves in the same way as\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 40cc9c4a23..dd873ec433 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3975,31 +3975,11 @@ static const char *const fast_import_usage[] = {\n \n static void parse_argv(struct fast_import_state *state)\n {\n-\tunsigned int i;\n-\n-\tfor (i = 1; i < state->argc; i++) {\n-\t\tconst char *a = state->argv[i];\n-\n-\t\tif (*a != '-' || !strcmp(a, \"--\"))\n-\t\t\tbreak;\n-\n-\t\tif (!skip_prefix(a, \"--\", &a))\n-\t\t\tdie(_(\"unknown option %s\"), a);\n-\n-\t\tif (parse_one_option(state, a))\n-\t\t\tcontinue;\n-\n-\t\tif (parse_one_feature(state, a, 0))\n-\t\t\tcontinue;\n-\n-\t\tif (skip_prefix(a, \"cat-blob-fd=\", &a)) {\n-\t\t\toption_cat_blob_fd(state, a);\n-\t\t\tcontinue;\n-\t\t}\n+\tint argc = parse_options(state->argc, state->argv, state->prefix,\n+\t\t\t\t state->option, fast_import_usage,\n+\t\t\t\t PARSE_OPT_KEEP_ARGV0);\n \n-\t\tdie(_(\"unknown option --%s\"), a);\n-\t}\n-\tif (i != state->argc)\n+\tif (argc > 1)\n \t\tusage_with_options(fast_import_usage, state->option);\n \n \tstate->seen_data_command = 1;\n@@ -4135,11 +4115,6 @@ int cmd_fast_import(int argc,\n {\n \tstruct fast_import_state state;\n \n-\t/*\n-\t * NEEDSWORK: For now this is used only to render\n-\t * `-h`/`--help-all` usage messages. The actual parsing is\n-\t * done by parse_one_option()/parse_one_feature().\n-\t */\n \tstruct option fast_import_options[] = {\n \t\tOPT_GROUP(N_(\"Common\")),\n \t\tOPT_CALLBACK_F(0, \"date-format\", NULL, N_(\"fmt\"),\n@@ -4230,6 +4205,16 @@ int cmd_fast_import(int argc,\n \t * \"feature\" lines at the start of the stream (which allows the command\n \t * line to override stream data). But we must do an early parse of any\n \t * command-line options that impact how we interpret the feature lines.\n+\t *\n+\t * NEEDSWORK: This scan only matches the exact \"--allow-unsafe-features\"\n+\t * spelling and stops at the first argument that doesn't start with a\n+\t * dash. As parse_options() below also accepts unambiguous abbreviations\n+\t * and values separated by a space from their option, the two disagree\n+\t * for command lines like \"--allow-unsafe\" or \"--depth 5\n+\t * --allow-unsafe-features\": parse_options() accepts the option, but\n+\t * this scan doesn't see it, so unsafe features from the stream are\n+\t * still refused. This errs on the safe side, but should be fixed by\n+\t * teaching this scan about the options that take a value.\n \t */\n \tfor (int i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\ndiff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh\nindex fe6c2617ac..d9de2ef0d8 100755\n--- a/t/t9300-fast-import.sh\n+++ b/t/t9300-fast-import.sh\n@@ -2827,6 +2827,13 @@ test_expect_success 'R: unknown commandline options are rejected' '\\\n \ttest_must_fail git fast-import --non-existing-option < /dev/null\n '\n \n+test_expect_success 'R: feature-only names are rejected on the command line' '\n+\tfor opt in --alias --get-mark --ls --notes\n+\tdo\n+\t\ttest_must_fail git fast-import \"$opt\" </dev/null || return 1\n+\tdone\n+'\n+\n test_expect_success 'R: die on invalid option argument' '\n \techo \"option git active-branches=-5\" |\n \ttest_must_fail git fast-import &&\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550260","messageId":"20260811083314.2023489-13-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 12/12] fast-import: remove useless from_stream argument","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:14Z","receivedAt":"2026-08-11T08:33:56Z","isPatch":true,"body":"Now that a previous commit has removed a call to parse_one_feature()\nfrom parse_argv(), the former is always called with its `from_stream`\nargument set to 1.\n\nLet's take advantage of that to simplify and cleanup the code a bit.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n builtin/fast-import.c | 20 +++++++++-----------\n 1 file changed, 9 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex dd873ec433..4e3c960150 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3867,30 +3867,28 @@ static int parse_one_option(struct fast_import_state *state, const char *option)\n }\n \n static void check_unsafe_feature(struct fast_import_state *state,\n-\t\t\t\t const char *feature,\n-\t\t\t\t int from_stream)\n+\t\t\t\t const char *feature)\n {\n-\tif (from_stream && !state->allow_unsafe_features)\n+\tif (!state->allow_unsafe_features)\n \t\tdie(_(\"feature '%s' forbidden in input without --allow-unsafe-features\"),\n \t\t    feature);\n }\n \n static int parse_one_feature(struct fast_import_state *state,\n-\t\t\t     const char *feature,\n-\t\t\t     int from_stream)\n+\t\t\t     const char *feature)\n {\n \tconst char *arg;\n \n \tif (skip_prefix(feature, \"date-format=\", &arg)) {\n \t\toption_date_format(arg);\n \t} else if (skip_prefix(feature, \"import-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, \"import-marks\", from_stream);\n-\t\toption_import_marks(state, arg, from_stream, 0);\n+\t\tcheck_unsafe_feature(state, \"import-marks\");\n+\t\toption_import_marks(state, arg, 1, 0);\n \t} else if (skip_prefix(feature, \"import-marks-if-exists=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\", from_stream);\n-\t\toption_import_marks(state, arg, from_stream, 1);\n+\t\tcheck_unsafe_feature(state, \"import-marks-if-exists\");\n+\t\toption_import_marks(state, arg, 1, 1);\n \t} else if (skip_prefix(feature, \"export-marks=\", &arg)) {\n-\t\tcheck_unsafe_feature(state, feature, from_stream);\n+\t\tcheck_unsafe_feature(state, feature);\n \t\toption_export_marks(state, arg);\n \t} else if (!strcmp(feature, \"alias\")) {\n \t\t; /* Don't die - this feature is supported */\n@@ -3924,7 +3922,7 @@ static void parse_feature(struct fast_import_state *state, const char *feature)\n \tif (state->seen_data_command)\n \t\tdie(_(\"got feature command '%s' after data command\"), feature);\n \n-\tif (parse_one_feature(state, feature, 1))\n+\tif (parse_one_feature(state, feature))\n \t\treturn;\n \n \tdie(_(\"this version of fast-import does not support feature %s.\"), feature);\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550261","messageId":"20260811083314.2023489-2-christian.couder@gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"[PATCH v3 01/12] parse-options: introduce OPT_HIDDEN_GROUP","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-11T08:33:03Z","receivedAt":"2026-08-11T08:34:12Z","isPatch":true,"body":"Hidden options are not shown by `git <cmd> -h`, but are still shown by\n`git <cmd> --help-all`. If there are a lot of hidden options or if they\ndon't belong to the same categories as other options, there is\ncurrently no way to properly group them.\n\nUsing `OPT_GROUP(\"Foo\")` means that \"Foo\" will always be shown which we\ndon't want if that group contains only hidden options.\n\nTo provide a way to have groups shown only when hidden options are\nshown, let's implement an OPT_HIDDEN_GROUP macro.\n\nTo test this new macro, let's also improve `test-tool parse-options`\nand test its output with `--help-all`.\n\nSigned-off-by: Christian Couder <christian.couder@gmail.com>\n---\n parse-options.c               |  4 ++--\n parse-options.h               |  5 +++++\n t/helper/test-parse-options.c |  4 ++++\n t/t0040-parse-options.sh      | 25 ++++++++++++++++++++++++-\n 4 files changed, 35 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 08c21d9fc0..4519ead9dc 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1414,6 +1414,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \n \t\tif (opts->type == OPTION_SUBCOMMAND)\n \t\t\tcontinue;\n+\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n+\t\t\tcontinue;\n \t\tif (opts->type == OPTION_GROUP) {\n \t\t\tfputc('\\n', outfile);\n \t\t\tneed_newline = 0;\n@@ -1421,8 +1423,6 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t\t\t\tfprintf(outfile, \"%s\\n\", _(opts->help));\n \t\t\tcontinue;\n \t\t}\n-\t\tif (!full && (opts->flags & PARSE_OPT_HIDDEN))\n-\t\t\tcontinue;\n \n \t\tif (need_newline) {\n \t\t\tfputc('\\n', outfile);\ndiff --git a/parse-options.h b/parse-options.h\nindex 3ec8ba5cc8..d7f896a933 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -237,6 +237,11 @@ struct option {\n \t.type = OPTION_GROUP, \\\n \t.help = (h), \\\n }\n+#define OPT_HIDDEN_GROUP(h) { \\\n+\t.type = OPTION_GROUP, \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_HIDDEN, \\\n+}\n #define OPT_BIT(s, l, v, h, b)      OPT_BIT_F(s, l, v, h, b, 0)\n #define OPT_BITOP(s, l, v, h, set, clear) { \\\n \t.type = OPTION_BITOP, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 68579d83f3..f181f0c02d 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -209,6 +209,10 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Alias\"),\n \t\tOPT_STRING('A', \"alias-source\", &string, \"string\", \"get a string\"),\n \t\tOPT_ALIAS('Z', \"alias-target\", \"alias-source\"),\n+\t\tOPT_HIDDEN_GROUP(\"Hidden options\"),\n+\t\tOPT_HIDDEN_BOOL(0, \"hidden-bool\", &boolean, \"get a boolean\"),\n+\t\tOPT_INTEGER_F('k', \"hidden-integer\", &integer, \"get a integer\",\n+\t\t\t      PARSE_OPT_HIDDEN),\n \t\tOPT_END(),\n \t};\n \tint ret = 0;\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex a22533f9ed..449fff4d34 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -7,7 +7,7 @@ test_description='our own option parser'\n \n . ./test-lib.sh\n \n-cat >expect <<\\EOF\n+cat >expect-part1 <<\\EOF\n usage: test-tool parse-options <options>\n \n     A helper function for the parse-options API.\n@@ -41,6 +41,9 @@ String options\n     --[no-]string2 <str>  get another string\n     --[no-]st <st>        get another string (pervert ordering)\n     -o <str>              get another string\n+EOF\n+\n+cat >expect-part2 <<\\EOF\n     --longhelp            help text of this entry\n                           spans multiple lines\n     --[no-]list <str>     add str to list\n@@ -67,12 +70,32 @@ Alias\n \n EOF\n \n+cat >expect-noop <<\\EOF\n+    --[no-]obsolete       no-op (backward compatibility)\n+EOF\n+\n+cat >expect-hidden <<\\EOF\n+Hidden options\n+    --[no-]hidden-bool    get a boolean\n+    -k, --[no-]hidden-integer <n>\n+                          get a integer\n+\n+EOF\n+\n test_expect_success 'test help' '\n+\tcat expect-part1 expect-part2 >expect &&\n \ttest-tool parse-options -h >output 2>output.err &&\n \ttest_must_be_empty output.err &&\n \ttest_cmp expect output\n '\n \n+test_expect_success 'test --help-all shows hidden group and options' '\n+\tcat expect-part1 expect-noop expect-part2 expect-hidden >expect-help-all &&\n+\ttest-tool parse-options --help-all >output 2>output.err &&\n+\ttest_must_be_empty output.err &&\n+\ttest_cmp expect-help-all output\n+'\n+\n mv expect expect.err\n \n check () {\n-- \n2.55.0.530.gdb3615d990.dirty\n\n"},{"id":"550451","messageId":"CABPp-BH4MmEL5vZv7ZoUufT8quk4vCq==Ef_XAfxjj9U-WBsEw@mail.gmail.com","threadId":"66022","inReplyTo":"20260811083314.2023489-1-christian.couder@gmail.com","subject":"Re: [PATCH v3 00/12] fast-import: standardize usage string and SYNOPSIS","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-12T20:29:47Z","receivedAt":"2026-08-12T20:30:02Z","isPatch":true,"body":"On Tue, Aug 11, 2026 at 1:33 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> Changes since v2\n> ================\n>\n> Thanks to Elijah for reviewing v2 and Junio for reviewing v1.\n>\n> The series has been rebased on top of recent master at 010afd3166 (The\n> 12th batch, 2026-08-07).\n>\n>   - Patch 2/12 now describes `PARSE_OPT_LASTARG_DEFAULT` the way Elijah\n>     suggested, which matches what \"parse-options.h\" says, and mentions\n>     that it should not be combined with `PARSE_OPT_OPTARG`.\n>\n>   - Patch 7/12 now wraps the function signatures that grew too long\n>     when they gained a 'struct fast_import_state *state' argument, with\n>     one parameter per line. Patches 8/12 and 12/12 have been adjusted\n>     accordingly, as they touch two of these signatures again.\n>\n>   - Patch 11/12 now also lists two other behavior changes that Elijah\n>     spotted: value-taking options accept the space-separated\n>     `--opt value` form, and a bare or trailing `--` is now accepted.\n>\n>   - Patch 11/12 also documents, both in a NEEDSWORK code comment and\n>     in \"Documentation/git-fast-import.adoc\", the third behavior change\n>     Elijah spotted about `--allow-unsafe-features`: as the early scan\n>     for that option only matches its exact spelling and stops at the\n>     first non-option argument, it disagrees with parse_options() for\n>     command lines like `--allow-unsafe` or\n>     `--depth 5 --allow-unsafe-features`. This errs on the safe side,\n>     as unsafe `feature` commands from the stream are refused in that\n>     case.\n>\n>     I plan to work on a follow-up series that will improve on this by\n>     teaching the early scan about the options that take a value. It\n>     looks like the parse-options API could provide some helpers for\n>     this, which might benefit other commands like `git` itself (see\n>     handle_options() in \"git.c\") that need to look at some options\n>     before the actual option parsing.\n\nThanks, this version addresses all my feedback on the previous round,\nand I spotted no new issues introduced.  Looks good to me.\n"}]}