{"thread":{"id":"59619","subject":"[PATCH 0/8] fetch: introduce machine-parseable output","startedAt":"2023-04-19T12:31:59Z","lastAt":"2023-05-15T05:15:56Z","messageCount":120,"participants":["Patrick Steinhardt","Felipe Contreras","Glen Choo","Jacob Keller","Junio C Hamano","SZEDER Gábor","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"475684","messageId":"cover.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":null,"subject":"[PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:22Z","receivedAt":"2023-04-19T12:31:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the second part of my quest to introduce a machine-parseable\noutput for git-fetch(1) after the initial refactorings that have been\nmerged via e9dffbc7f1 (Merge branch 'ps/fetch-ref-update-reporting',\n2023-04-06).\n\nParsing the output of fetches is mostly impossible. It prettifies\nreference names that are about to be updated, doesn't print the old and\nnew object IDs the refs are being updated from and to, and prints all of\nthat information in nice columns. In short, it is designed to be read by\nhumans rather than machines.\n\nThis makes it hard to use in a script way though, e.g. to learn about\nwhich references actually have been updated or which have not been\nupdated. This patch series intends to fix that by introducing a new\nmachine-parseable interface:\n\n```\n$ git fetch --output-format=porcelain --no-progress\n  fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n* 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n* 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n* 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n* 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n* 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n```\n\nThe series is structured as following:\n\n    - Patches 1 and 2 improve test coverage for output formats.\n\n    - Patch 3 fixes a bug with the current output format.\n\n    - Patch 4 to 6 perform some preliminary refactorings.\n\n    - Patch 7 introduces a new `--output-format=` option for\n      git-fetch(1) that allows the user to configure the output more\n      directly.\n\n    - Patch 8 introduces the new \"porcelain\" output format.\n\nPatrick\n\nPatrick Steinhardt (8):\n  fetch: split out tests for output format\n  fetch: add a test to exercise invalid output formats\n  fetch: fix missing from-reference when fetching HEAD:foo\n  fetch: introduce `display_format` enum\n  fetch: move display format parsing into main function\n  fetch: move option related variables into main function\n  fetch: introduce new `--output-format` option\n  fetch: introduce machine-parseable \"porcelain\" output format\n\n Documentation/config/fetch.txt  |   4 +-\n Documentation/fetch-options.txt |   5 +\n Documentation/git-fetch.txt     |  17 +-\n builtin/fetch.c                 | 406 +++++++++++++++++++-------------\n t/t5510-fetch.sh                |  53 -----\n t/t5574-fetch-output.sh         | 209 ++++++++++++++++\n 6 files changed, 475 insertions(+), 219 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\n-- \n2.40.0\n\n"},{"id":"475685","messageId":"0d0d50d14c557f8313747e7d0e104c2c0819dab9.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 1/8] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:26Z","receivedAt":"2023-04-19T12:32:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new porcelain mode for the output of\ngit-fetch(1). As part of that we'll be introducing a set of new tests\nthat only relate to the output of this command.\n\nSplit out tests that exercise the output format of git-fetch(1) so that\nit becomes easier to verify this functionality as a standalone unit. As\nthe tests assume that the default branch is called \"main\" we set up the\ncorresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\naccordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5510-fetch.sh        | 53 ----------------------------------\n t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 63 insertions(+), 53 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex dc44da9c79..4f289063ce 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1118,59 +1118,6 @@ test_expect_success 'fetching with auto-gc does not lock up' '\n \t)\n '\n \n-test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n-\ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n-\tlooooooooooooong-tag -> looooooooooooong-tag\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n-\ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n-\textraaa    -> *\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--no-show-forced-updates' '\n-\tmkdir forced-updates &&\n-\t(\n-\t\tcd forced-updates &&\n-\t\tgit init &&\n-\t\ttest_commit 1 &&\n-\t\ttest_commit 2\n-\t) &&\n-\tgit clone forced-updates forced-update-clone &&\n-\tgit clone forced-updates no-forced-update-clone &&\n-\tgit -C forced-updates reset --hard HEAD~1 &&\n-\t(\n-\t\tcd forced-update-clone &&\n-\t\tgit fetch --show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep \"(forced update)\" output\n-\t) &&\n-\t(\n-\t\tcd no-forced-update-clone &&\n-\t\tgit fetch --no-show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep ! \"(forced update)\" output\n-\t)\n-'\n-\n for section in fetch transfer\n do\n \ttest_expect_success \"$section.hideRefs affects connectivity check\" '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nnew file mode 100755\nindex 0000000000..f91b654d38\n--- /dev/null\n+++ b/t/t5574-fetch-output.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='git fetch output format'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'fetch aligned output' '\n+\tgit clone . full-output &&\n+\ttest_commit looooooooooooong-tag &&\n+\t(\n+\t\tcd full-output &&\n+\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain                 -> origin/main\n+\tlooooooooooooong-tag -> looooooooooooong-tag\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'fetch compact output' '\n+\tgit clone . compact &&\n+\ttest_commit extraaa &&\n+\t(\n+\t\tcd compact &&\n+\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain       -> origin/*\n+\textraaa    -> *\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-show-forced-updates' '\n+\tmkdir forced-updates &&\n+\t(\n+\t\tcd forced-updates &&\n+\t\tgit init &&\n+\t\ttest_commit 1 &&\n+\t\ttest_commit 2\n+\t) &&\n+\tgit clone forced-updates forced-update-clone &&\n+\tgit clone forced-updates no-forced-update-clone &&\n+\tgit -C forced-updates reset --hard HEAD~1 &&\n+\t(\n+\t\tcd forced-update-clone &&\n+\t\tgit fetch --show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep \"(forced update)\" output\n+\t) &&\n+\t(\n+\t\tcd no-forced-update-clone &&\n+\t\tgit fetch --no-show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep ! \"(forced update)\" output\n+\t)\n+'\n+\n+test_done\n-- \n2.40.0\n\n"},{"id":"475686","messageId":"29d2c58914b7270a4603cd65565c06ff74355e96.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 2/8] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:31Z","receivedAt":"2023-04-19T12:32:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add a testcase that exercises the logic when an invalid output format is\npassed via the `fetch.output` configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5574-fetch-output.sh | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex f91b654d38..0e45c27007 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -7,6 +7,23 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+test_expect_success 'fetch with invalid output format configuration' '\n+\ttest_when_finished \"rm -rf clone\" &&\n+\tgit clone . clone &&\n+\n+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch aligned output' '\n \tgit clone . full-output &&\n \ttest_commit looooooooooooong-tag &&\n-- \n2.40.0\n\n"},{"id":"475687","messageId":"596e12f03a296d753ee6fe8face9522edc9e397e.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:35Z","receivedAt":"2023-04-19T12:32:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When displaying reference updates, we print a line that looks similar to\nthe following:\n\n```\n * branch               master          -> master\n```\n\nThe \"branch\" bit changes depending on what kind of reference we're\nupdating, while both of the right-hand references are computed by\nstripping well-known prefixes like \"refs/heads/\" or \"refs/tags\".\n\nThe logic is kind of intertwined though and not easy to follow: we\nprecompute both the kind (e.g. \"branch\") and the what, which is the\nabbreviated remote reference name, in `store_updated_refs()` and then\npass it down the call chain to `display_ref_update()`.\n\nThere is a set of different cases here:\n\n    - When the remote reference name is \"HEAD\" we assume no kind and\n      will thus instead print \"[new ref]\". We keep what at the empty\n      string.\n\n    - When the remote reference name has a well-known prefix then the\n      kind would be \"branch\", \"tag\" or \"remote-tracking branch\". The\n      what is the reference with the well-known prefix stripped and in\n      fact matches the output that `prettify_refname()` would return.\n\n    - Otherwise, we'll again assume no kind and keep the what set to the\n      fully qualified reference name.\n\nNow there is a bug with the first case here, where the remote reference\nname is \"HEAD\". As noted, \"what\" will be set to the empty string. And\nthat seems to be intentional because we also use this information to\nupdate the FETCH_HEAD, and in case we're updating HEAD we seemingly\ndon't want to append that to our FETCH_HEAD value.\n\nBut as mentioned, we also use this value to display reference updates.\nAnd while the call to `display_ref_update()` correctly figures out that\nwe meant \"HEAD\" when `what` is empty, the call to `update_local_ref()`\ndoesn't. `update_local_ref()` will then call `display_ref_update()` with\nthe empty string and cause the following broken output:\n\n```\n$ git fetch --dry-run origin HEAD:foo\nFrom https://github.com/git/git\n * [new ref]                          -> foo\n```\n\nThe HEAD string is clearly missing from the left-hand side of the arrow,\nwhich is further stressed by the point that the following commands work\nas expected:\n\n```\n$ git fetch --dry-run origin HEAD\nFrom https://github.com/git/git\n * branch                  HEAD       -> FETCH_HEAD\n\n$ git fetch --dry-run origin master\nFrom https://github.com/git/git\n * branch                  master     -> FETCH_HEAD\n * branch                  master     -> origin/master\n```\n\nFix this bug by instead unconditionally passing the full reference name\nto `display_ref_update()` which learns to call `prettify_refname()` on\nit. This does fix the above bug and is otherwise functionally the same\nas `prettify_refname()` would only ever strip the well-known prefixes\njust as intended. So at the same time, this also simplifies the code a\nbit.\n\nNote that this patch also changes formatting of the block that computes\nthe \"kind\" and \"what\" variables. This is done on purpose so that it is\npart of the diff, hopefully making the change easier to comprehend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c         | 37 +++++++++++++++++++------------------\n t/t5574-fetch-output.sh | 19 +++++++++++++++++++\n 2 files changed, 38 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c310d89878..7c64f0c562 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t}\n \n \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\tremote = prettify_refname(remote);\n+\tlocal = prettify_refname(local);\n \n \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n \tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n+\t\tprint_remote_to_local(display_state, remote, local);\n \telse\n-\t\tprint_compact(display_state, remote, prettify_refname(local));\n+\t\tprint_compact(display_state, remote, local);\n \tif (error)\n \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n \tstrbuf_addch(&display_state->buf, '\\n');\n@@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n-\t\t\t    const char *remote, const struct ref *remote_ref,\n+\t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width)\n {\n \tstruct commit *current = NULL, *updated;\n@@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn r;\n \t}\n \n@@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = \"\";\n-\t\t\t}\n-\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n \t\t\t\tkind = \"branch\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n \t\t\t\tkind = \"tag\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n \t\t\t\tkind = \"remote-tracking branch\";\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = rm->name;\n \t\t\t}\n@@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t\t  display_state->url_len);\n \n \t\t\tif (ref) {\n-\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n+\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n \t\t\t\t\t\t       rm, summary_width);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n@@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t */\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n-\t\t\t\t\t\t   *what ? what : \"HEAD\",\n+\t\t\t\t\t\t   rm->name,\n \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n \t\t\t}\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 0e45c27007..55f0f05b6a 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -54,6 +54,25 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch output with HEAD and --dry-run' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\n+\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * branch            HEAD       -> FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         HEAD       -> foo\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.0\n\n"},{"id":"475688","messageId":"8571363be1943ed12dd5295593509bae92194933.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 4/8] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:39Z","receivedAt":"2023-04-19T12:32:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We currently have two different display formats in git-fetch(1) with the\n\"full\" and \"compact\" formats. This is tracked with a boolean value that\nsimply denotes whether the display format is supposed to be compacted\nor not. This works reasonably well while there are only two formats, but\nwe're about to introduce another format that will make this a bit more\nawkward to use.\n\nIntroduce a `enum display_format` that is more readily extensible.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 101 ++++++++++++++++++++++++++++++------------------\n 1 file changed, 64 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7c64f0c562..e03fcd1b2f 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -48,11 +48,17 @@ enum {\n \tTAGS_SET = 2\n };\n \n+enum display_format {\n+\tDISPLAY_FORMAT_UNKNOWN = 0,\n+\tDISPLAY_FORMAT_FULL,\n+\tDISPLAY_FORMAT_COMPACT,\n+};\n+\n struct display_state {\n \tstruct strbuf buf;\n \n \tint refcol_width;\n-\tint compact_format;\n+\tenum display_format format;\n \n \tchar *url;\n \tint url_len, shown_url;\n@@ -784,7 +790,6 @@ static int refcol_width(const struct ref *ref, int compact_format)\n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n \t\t\t       const char *raw_url)\n {\n-\tstruct ref *rm;\n \tconst char *format = \"full\";\n \tint i;\n \n@@ -809,31 +814,42 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \tgit_config_get_string_tmp(\"fetch.output\", &format);\n \tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->compact_format = 0;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n \telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->compact_format = 1;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = 10;\n-\tfor (rm = ref_map; rm; rm = rm->next) {\n-\t\tint width;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tstruct ref *rm;\n \n-\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n-\t\t    !rm->peer_ref ||\n-\t\t    !strcmp(rm->name, \"HEAD\"))\n-\t\t\tcontinue;\n+\t\tdisplay_state->refcol_width = 10;\n+\t\tfor (rm = ref_map; rm; rm = rm->next) {\n+\t\t\tint width;\n \n-\t\twidth = refcol_width(rm, display_state->compact_format);\n+\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n+\t\t\t    !rm->peer_ref ||\n+\t\t\t    !strcmp(rm->name, \"HEAD\"))\n+\t\t\t\tcontinue;\n \n-\t\t/*\n-\t\t * Not precise calculation for compact mode because '*' can\n-\t\t * appear on the left hand side of '->' and shrink the column\n-\t\t * back.\n-\t\t */\n-\t\tif (display_state->refcol_width < width)\n-\t\t\tdisplay_state->refcol_width = width;\n+\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n+\n+\t\t\t/*\n+\t\t\t * Not precise calculation for compact mode because '*' can\n+\t\t\t * appear on the left hand side of '->' and shrink the column\n+\t\t\t * back.\n+\t\t\t */\n+\t\t\tif (display_state->refcol_width < width)\n+\t\t\t\tdisplay_state->refcol_width = width;\n+\t\t}\n+\n+\t\tbreak;\n+\t}\n+\tdefault:\n+\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n \t}\n }\n \n@@ -904,30 +920,41 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *remote, const char *local,\n \t\t\t       int summary_width)\n {\n-\tint width;\n-\n \tif (verbosity < 0)\n \t\treturn;\n \n \tstrbuf_reset(&display_state->buf);\n \n-\tif (!display_state->shown_url) {\n-\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n-\t\t\t    display_state->url_len, display_state->url);\n-\t\tdisplay_state->shown_url = 1;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tint width;\n+\n+\t\tif (!display_state->shown_url) {\n+\t\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n+\t\t\t\t    display_state->url_len, display_state->url);\n+\t\t\tdisplay_state->shown_url = 1;\n+\t\t}\n+\n+\t\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\t\tremote = prettify_refname(remote);\n+\t\tlocal = prettify_refname(local);\n+\n+\t\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n+\n+\t\tif (display_state->format != DISPLAY_FORMAT_COMPACT)\n+\t\t\tprint_remote_to_local(display_state, remote, local);\n+\t\telse\n+\t\t\tprint_compact(display_state, remote, local);\n+\n+\t\tif (error)\n+\t\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\n+\t\tbreak;\n \t}\n-\n-\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n-\tremote = prettify_refname(remote);\n-\tlocal = prettify_refname(local);\n-\n-\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n-\tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, local);\n-\telse\n-\t\tprint_compact(display_state, remote, local);\n-\tif (error)\n-\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n \tfputs(display_state->buf.buf, stderr);\n-- \n2.40.0\n\n"},{"id":"475689","messageId":"d98c3ee0ceac73a62f279dec645e1f4c0b170b68.1681906948.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 5/8] fetch: move display format parsing into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:43Z","receivedAt":"2023-04-19T12:32:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Parsing the display format happens inside of `display_state_init()`. As\nwe only need to check for a simple config entry, this is a natural\nlocation to put this code as it means that display-state logic is neatly\ncontained in a single location.\n\nWe're about to introduce a output format though that is intended to be\nparseable by machines, for example inside of a script. In that case it\nbecomes a bit awkward of an interface if you have to call git-fetch(1)\nwith the `fetch.output` config key set. We're thus going to introduce a\nnew `--output-format` switch for git-fetch(1) so that the output format\ncan be configured more directly.\n\nThis means we'll have to hook parsing of the display format into the\ncommand line options parser. Having the code to determine the actual\noutput format scattered across two different sites is hard to reason\nabout though.\n\nRefactor the code such that callers are expected to pass the display\nformat that is to be used into `display_state_init()`. This allows us to\nlift up the code into the main function, where we can then hook it into\ncommand line options parser in a follow-up commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 41 ++++++++++++++++++++++++-----------------\n 1 file changed, 24 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e03fcd1b2f..bcc156a9ce 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -788,14 +788,13 @@ static int refcol_width(const struct ref *ref, int compact_format)\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n-\t\t\t       const char *raw_url)\n+\t\t\t       const char *raw_url, enum display_format format)\n {\n-\tconst char *format = \"full\";\n \tint i;\n \n \tmemset(display_state, 0, sizeof(*display_state));\n-\n \tstrbuf_init(&display_state->buf, 0);\n+\tdisplay_state->format = format;\n \n \tif (raw_url)\n \t\tdisplay_state->url = transport_anonymize_url(raw_url);\n@@ -812,15 +811,6 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \tif (verbosity < 0)\n \t\treturn;\n \n-\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n-\telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n-\telse\n-\t\tdie(_(\"invalid value for '%s': '%s'\"),\n-\t\t    \"fetch.output\", format);\n-\n \tswitch (display_state->format) {\n \tcase DISPLAY_FORMAT_FULL:\n \tcase DISPLAY_FORMAT_COMPACT: {\n@@ -1614,7 +1604,8 @@ static int backfill_tags(struct display_state *display_state,\n }\n \n static int do_fetch(struct transport *transport,\n-\t\t    struct refspec *rs)\n+\t\t    struct refspec *rs,\n+\t\t    enum display_format display_format)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1700,7 +1691,7 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n-\tdisplay_state_init(&display_state, ref_map, transport->url);\n+\tdisplay_state_init(&display_state, ref_map, transport->url, display_format);\n \n \tif (atomic_fetch) {\n \t\ttransaction = ref_transaction_begin(&err);\n@@ -2076,7 +2067,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n-\t\t     int prune_tags_ok, int use_stdin_refspecs)\n+\t\t     int prune_tags_ok, int use_stdin_refspecs,\n+\t\t     enum display_format display_format)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2143,7 +2135,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs);\n+\texit_code = do_fetch(gtransport, &rs, display_format);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2155,6 +2147,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tconst char *bundle_uri;\n+\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n \tint result = 0;\n@@ -2181,6 +2174,19 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n \n+\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n+\t\tconst char *format = \"full\";\n+\n+\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n+\t\tif (!strcasecmp(format, \"full\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n+\t\telse if (!strcasecmp(format, \"compact\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t    \"fetch.output\", format);\n+\t}\n+\n \tif (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)\n \t\trecurse_submodules = recurse_submodules_cli;\n \n@@ -2309,7 +2315,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t} else if (remote) {\n \t\tif (filter_options.choice || has_promisor_remote())\n \t\t\tfetch_one_setup_partial(remote);\n-\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n+\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n+\t\t\t\t   display_format);\n \t} else {\n \t\tint max_children = max_jobs;\n \n-- \n2.40.0\n\n"},{"id":"475690","messageId":"640a8840e1b87ec8ea985dd2299e1bab584d516d.1681906949.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 6/8] fetch: move option related variables into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:47Z","receivedAt":"2023-04-19T12:32:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The options of git-fetch(1) which we pass to `parse_options()` are\ndeclared globally in `builtin/fetch.c`. This means we're forced to use\nglobal variables for all the options, which is more likely to cause\nconfusion than explicitly passing state around.\n\nRefactor the code to move the options into `cmd_fetch()`. Move variables\nthat were previously forced to be declared globally and which are only\nused by `cmd_fetch()` into function-local scope.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 194 ++++++++++++++++++++++++------------------------\n 1 file changed, 98 insertions(+), 96 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex bcc156a9ce..81581b0033 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -79,9 +79,8 @@ static int all, append, dry_run, force, keep, multiple, update_head_ok;\n static int write_fetch_head = 1;\n static int verbosity, deepen_relative, set_upstream, refetch;\n static int progress = -1;\n-static int enable_auto_gc = 1;\n-static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;\n-static int max_jobs = -1, submodule_fetch_jobs_config = -1;\n+static int tags = TAGS_DEFAULT, update_shallow, deepen;\n+static int submodule_fetch_jobs_config = -1;\n static int fetch_parallel_config = 1;\n static int atomic_fetch;\n static enum transport_family family;\n@@ -92,17 +91,11 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n-static const char *submodule_prefix = \"\";\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n-static int fetch_write_commit_graph = -1;\n-static int stdin_refspecs = 0;\n-static int negotiate_only;\n \n static int git_fetch_config(const char *k, const char *v, void *cb)\n {\n@@ -160,92 +153,6 @@ static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-static struct option builtin_fetch_options[] = {\n-\tOPT__VERBOSITY(&verbosity),\n-\tOPT_BOOL(0, \"all\", &all,\n-\t\t N_(\"fetch from all remotes\")),\n-\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n-\t\t N_(\"set upstream for git pull/fetch\")),\n-\tOPT_BOOL('a', \"append\", &append,\n-\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n-\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n-\t\t N_(\"use atomic transaction to update references\")),\n-\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to upload pack on remote end\")),\n-\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n-\tOPT_BOOL('m', \"multiple\", &multiple,\n-\t\t N_(\"fetch from multiple remotes\")),\n-\tOPT_SET_INT('t', \"tags\", &tags,\n-\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n-\tOPT_SET_INT('n', NULL, &tags,\n-\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules fetched in parallel\")),\n-\tOPT_BOOL(0, \"prefetch\", &prefetch,\n-\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n-\tOPT_BOOL('p', \"prune\", &prune,\n-\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n-\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n-\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n-\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n-\t\t    N_(\"control recursive fetching of submodules\"),\n-\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"dry-run\", &dry_run,\n-\t\t N_(\"dry run\")),\n-\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n-\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n-\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n-\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n-\t\t    N_(\"allow updating of HEAD ref\")),\n-\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n-\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n-\t\t   N_(\"deepen history of shallow clone\")),\n-\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n-\t\t   N_(\"deepen history of shallow repository based on time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n-\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n-\t\t    N_(\"deepen history of shallow clone\")),\n-\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n-\t\t      N_(\"convert to a complete repository\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n-\t\t      N_(\"re-fetch without negotiating common commits\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n-\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n-\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n-\t\t   N_(\"default for recursive fetching of submodules \"\n-\t\t      \"(lower priority than config files)\"),\n-\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n-\t\t N_(\"accept refs that update .git/shallow\")),\n-\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n-\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n-\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV4),\n-\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV6),\n-\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n-\t\t\tN_(\"report that we have only objects reachable from this object\")),\n-\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n-\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n-\t\t N_(\"check for forced-updates on all updated branches\")),\n-\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n-\t\t N_(\"write the commit-graph after fetching\")),\n-\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n-\t\t N_(\"accept refspecs from stdin\")),\n-\tOPT_END()\n-};\n-\n static void unlock_pack(unsigned int flags)\n {\n \tif (gtransport)\n@@ -2145,13 +2052,108 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n-\tint i;\n \tconst char *bundle_uri;\n+\tconst char *submodule_prefix = \"\";\n \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n \tint result = 0;\n \tint prune_tags_ok = 1;\n+\tint enable_auto_gc = 1;\n+\tint unshallow = 0;\n+\tint max_jobs = -1;\n+\tint recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n+\tint recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n+\tint fetch_write_commit_graph = -1;\n+\tint stdin_refspecs = 0;\n+\tint negotiate_only = 0;\n+\tint i;\n+\n+\tstruct option builtin_fetch_options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_BOOL(0, \"all\", &all,\n+\t\t\t N_(\"fetch from all remotes\")),\n+\t\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n+\t\t\t N_(\"set upstream for git pull/fetch\")),\n+\t\tOPT_BOOL('a', \"append\", &append,\n+\t\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n+\t\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n+\t\t\t N_(\"use atomic transaction to update references\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to upload pack on remote end\")),\n+\t\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n+\t\tOPT_BOOL('m', \"multiple\", &multiple,\n+\t\t\t N_(\"fetch from multiple remotes\")),\n+\t\tOPT_SET_INT('t', \"tags\", &tags,\n+\t\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n+\t\tOPT_SET_INT('n', NULL, &tags,\n+\t\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules fetched in parallel\")),\n+\t\tOPT_BOOL(0, \"prefetch\", &prefetch,\n+\t\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n+\t\tOPT_BOOL('p', \"prune\", &prune,\n+\t\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n+\t\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n+\t\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n+\t\t\t    N_(\"control recursive fetching of submodules\"),\n+\t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n+\t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n+\t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n+\t\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n+\t\t\t    N_(\"allow updating of HEAD ref\")),\n+\t\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n+\t\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n+\t\t\t   N_(\"deepen history of shallow clone\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n+\t\t\t   N_(\"deepen history of shallow repository based on time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n+\t\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n+\t\t\t    N_(\"deepen history of shallow clone\")),\n+\t\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n+\t\t\t      N_(\"convert to a complete repository\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n+\t\t\t      N_(\"re-fetch without negotiating common commits\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n+\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n+\t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n+\t\t\t   N_(\"default for recursive fetching of submodules \"\n+\t\t\t      \"(lower priority than config files)\"),\n+\t\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n+\t\t\t N_(\"accept refs that update .git/shallow\")),\n+\t\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n+\t\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV4),\n+\t\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV6),\n+\t\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n+\t\t\t\tN_(\"report that we have only objects reachable from this object\")),\n+\t\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n+\t\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n+\t\t\t N_(\"check for forced-updates on all updated branches\")),\n+\t\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n+\t\t\t N_(\"write the commit-graph after fetching\")),\n+\t\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n+\t\t\t N_(\"accept refspecs from stdin\")),\n+\t\tOPT_END()\n+\t};\n \n \tpacket_trace_identity(\"fetch\");\n \n-- \n2.40.0\n\n"},{"id":"475691","messageId":"3b2cad066a3b3446fc335d6944a62bf79b0779bb.1681906949.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:51Z","receivedAt":"2023-04-19T12:32:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is only possible to configure the output format that git-fetch(1)\nuses by setting it via a config key. While this interface may be fine as\nlong as we only have the current \"full\" and \"compact\" output formats,\nwhere it is unlikely that the user will have to change them regularly.\nBut we're about to introduce a new machine-parseable interface where the\ncurrent mechanism feels a little bit indirect and rigid.\n\nIntroduce a new `--output-format` option that allows the user to change\nthe desired output format more directly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/fetch-options.txt |  5 ++++\n builtin/fetch.c                 | 17 +++++++++++\n t/t5574-fetch-output.sh         | 50 +++++++++++++++++++++++++--------\n 3 files changed, 60 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 622bd84768..654f96f79d 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -78,6 +78,11 @@ linkgit:git-config[1].\n --dry-run::\n \tShow what would be done, without making any changes.\n \n+--output-format::\n+\tControl how ref update status is printed. Valid values are\n+\t`full` and `compact`. Default value is `full`. See section\n+\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\n ifndef::git-pull[]\n --[no-]write-fetch-head::\n \tWrite the list of remote refs fetched in the `FETCH_HEAD`\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 81581b0033..22ba75a958 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2050,6 +2050,21 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \treturn exit_code;\n }\n \n+static int opt_parse_output_format(const struct option *opt, const char *arg, int unset)\n+{\n+\tenum display_format *format = opt->value;\n+\tif (unset || !arg)\n+\t\treturn 1;\n+\telse if (!strcmp(arg, \"full\"))\n+\t\t*format = DISPLAY_FORMAT_FULL;\n+\telse if (!strcmp(arg, \"compact\"))\n+\t\t*format = DISPLAY_FORMAT_COMPACT;\n+\telse\n+\t\treturn error(_(\"unsupported output format '%s'\"), arg);\n+\n+\treturn 0;\n+}\n+\n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tconst char *bundle_uri;\n@@ -2101,6 +2116,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n \t\t\t N_(\"dry run\")),\n+\t\tOPT_CALLBACK(0, \"output-format\", &display_format, N_(\"format\"), N_(\"output format\"),\n+\t\t\t     opt_parse_output_format),\n \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 55f0f05b6a..5144d5ed21 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -24,14 +24,37 @@ test_expect_success 'fetch with invalid output format configuration' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch with invalid output format via command line' '\n+\ttest_must_fail git fetch --output-format >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: option \\`output-format${SQ} requires a value\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git fetch --output-format= origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: unsupported output format ${SQ}${SQ}\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git fetch --output-format=garbage origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: unsupported output format ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n+\ttest_when_finished \"rm -rf full-cfg full-cli\" &&\n+\tgit clone . full-cfg &&\n+\tgit clone . full-cli &&\n \ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n+\n+\tgit -C full-cfg -c fetch.output=full fetch origin >actual-cfg 2>&1 &&\n+\tgit -C full-cli fetch --output-format=full origin >actual-cli 2>&1 &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\n+\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n \tcat >expect <<-\\EOF &&\n \tmain                 -> origin/main\n \tlooooooooooooong-tag -> looooooooooooong-tag\n@@ -40,13 +63,16 @@ test_expect_success 'fetch aligned output' '\n '\n \n test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n+\ttest_when_finished \"rm -rf compact-cfg compact-cli\" &&\n+\tgit clone . compact-cli &&\n+\tgit clone . compact-cfg &&\n \ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n+\n+\tgit -C compact-cfg -c fetch.output=compact fetch origin >actual-cfg 2>&1 &&\n+\tgit -C compact-cli fetch --output-format=compact origin >actual-cli 2>&1 &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\n+\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n \tcat >expect <<-\\EOF &&\n \tmain       -> origin/*\n \textraaa    -> *\n-- \n2.40.0\n\n"},{"id":"475692","messageId":"301138da039451519c6e60d6126e7756a54d346f.1681906949.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-19T12:31:55Z","receivedAt":"2023-04-19T12:32:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The output of git-fetch(1) is obviously designed for consumption by\nusers, only: we neatly columnize data, we abbreviate reference names, we\nprint neat arrows and we don't provide information about actual object\nIDs that have changed. This makes the output format basically unusable\nin the context of scripted invocations of git-fetch(1) that want to\nlearn about the exact changes that the command performs.\n\nIntroduce a new machine-parseable \"porcelain\" output format that is\nsupposed to fix this shortcoming. This output format is intended to\nprovide information about every reference that is about to be updated,\nthe old object ID that the reference has been pointing to and the new\nobject ID it will be updated to. Furthermore, the output format provides\nthe same flags as the human-readable format to indicate basic conditions\nfor each reference update like whether it was a fast-forward update, a\nbranch deletion, a rejected update or others.\n\nThe output format is quite simple:\n\n```\n<flag> <old-object-id> <new-object-id> <local-reference>\\n\n```\n\nWe assume two conditions which are generally true:\n\n    - The old and new object IDs have fixed known widths and cannot\n      contain spaces.\n\n    - References cannot contain newlines.\n\nWith these assumptions, the output format becomes unambiguously\nparseable. Furthermore, given that this output is designed to be\nconsumed by scripts, the machine-readable data is printed to stdout\ninstead of stderr like the human-readable output is. This is mostly done\nso that other data printed to stderr, like error messages or progress\nmeters, don't interfere with the parseable data.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/fetch.txt  |  4 +-\n Documentation/fetch-options.txt |  4 +-\n Documentation/git-fetch.txt     | 17 ++++++-\n builtin/fetch.c                 | 50 +++++++++++++++-----\n t/t5574-fetch-output.sh         | 84 +++++++++++++++++++++++++++++++++\n 5 files changed, 142 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/fetch.txt b/Documentation/config/fetch.txt\nindex 568f0f75b3..70734226c0 100644\n--- a/Documentation/config/fetch.txt\n+++ b/Documentation/config/fetch.txt\n@@ -52,8 +52,8 @@ fetch.pruneTags::\n \n fetch.output::\n \tControl how ref update status is printed. Valid values are\n-\t`full` and `compact`. Default value is `full`. See section\n-\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\t`full`, `compact` and `porcelain`. Default value is `full`.\n+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n \n fetch.negotiationAlgorithm::\n \tControl how information about the commits in the local repository\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 654f96f79d..5ca8a67fe8 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -80,8 +80,8 @@ linkgit:git-config[1].\n \n --output-format::\n \tControl how ref update status is printed. Valid values are\n-\t`full` and `compact`. Default value is `full`. See section\n-\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\t`full`, `compact` and `porcelain`. Default value is `full`.\n+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n \n ifndef::git-pull[]\n --[no-]write-fetch-head::\ndiff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\nindex fba66f1460..efd22cd372 100644\n--- a/Documentation/git-fetch.txt\n+++ b/Documentation/git-fetch.txt\n@@ -197,13 +197,26 @@ The output of \"git fetch\" depends on the transport method used; this\n section describes the output when fetching over the Git protocol\n (either locally or via ssh) and Smart HTTP protocol.\n \n-The status of the fetch is output in tabular form, with each line\n-representing the status of a single ref. Each line is of the form:\n+The output format can be chosen either via the `fetch.output` config\n+(see linkgit:git-config[1]), or via the `--output-format` switch.\n+Supported values include:\n+\n+For the `full` and `compact` output formats, the status of the fetch is\n+output in tabular, with each line representing the status of a single\n+ref. Each line is of the form:\n \n -------------------------------\n  <flag> <summary> <from> -> <to> [<reason>]\n -------------------------------\n \n+The `porcelain` output format is intended to be machine-parseable. In\n+contrast to the human-readable output formats it thus prints to standard\n+output instead of standard error. Each line is of the form:\n+\n+-------------------------------\n+<flag> <old-object-id> <new-object-id> <local-reference>\n+-------------------------------\n+\n The status of up-to-date refs is shown only if the --verbose option is\n used.\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 22ba75a958..a99e24448b 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,7 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_PORCELAIN,\n };\n \n struct display_state {\n@@ -745,8 +746,11 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\t/* We don't need to precompute anything here. */\n+\t\tbreak;\n \tdefault:\n-\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n }\n \n@@ -815,8 +819,12 @@ static void print_compact(struct display_state *display_state,\n static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *summary, const char *error,\n \t\t\t       const char *remote, const char *local,\n+\t\t\t       const struct object_id *old_oid,\n+\t\t\t       const struct object_id *new_oid,\n \t\t\t       int summary_width)\n {\n+\tFILE *f = stderr;\n+\n \tif (verbosity < 0)\n \t\treturn;\n \n@@ -849,12 +857,17 @@ static void display_ref_update(struct display_state *display_state, char code,\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\tstrbuf_addf(&display_state->buf, \"%c %s %s %s\", code,\n+\t\t\t    oid_to_hex(old_oid), oid_to_hex(new_oid), local);\n+\t\tf = stdout;\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n-\tfputs(display_state->buf.buf, stderr);\n+\tfputs(display_state->buf.buf, f);\n }\n \n static int update_local_ref(struct ref *ref,\n@@ -872,7 +885,8 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -885,7 +899,8 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -896,12 +911,14 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -934,7 +951,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn r;\n \t}\n \n@@ -956,7 +974,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -968,12 +987,14 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1214,7 +1235,9 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n \t\t\t\t\t\t   rm->name,\n-\t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n+\t\t\t\t\t\t   \"FETCH_HEAD\",\n+\t\t\t\t\t\t   &rm->new_oid, &rm->old_oid,\n+\t\t\t\t\t\t   summary_width);\n \t\t\t}\n \t\t}\n \t}\n@@ -1354,6 +1377,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\tdisplay_ref_update(display_state, '-', _(\"[deleted]\"), NULL,\n \t\t\t\t\t   _(\"(none)\"), ref->name,\n+\t\t\t\t\t   &ref->new_oid, &ref->old_oid,\n \t\t\t\t\t   summary_width);\n \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n \t\t}\n@@ -2059,6 +2083,8 @@ static int opt_parse_output_format(const struct option *opt, const char *arg, in\n \t\t*format = DISPLAY_FORMAT_FULL;\n \telse if (!strcmp(arg, \"compact\"))\n \t\t*format = DISPLAY_FORMAT_COMPACT;\n+\telse if (!strcmp(arg, \"porcelain\"))\n+\t\t*format = DISPLAY_FORMAT_PORCELAIN;\n \telse\n \t\treturn error(_(\"unsupported output format '%s'\"), arg);\n \n@@ -2201,6 +2227,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n \t\telse if (!strcasecmp(format, \"compact\"))\n \t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse if (!strcasecmp(format, \"porcelain\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_PORCELAIN;\n \t\telse\n \t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t\t    \"fetch.output\", format);\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 5144d5ed21..28d02a93e4 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -80,6 +80,72 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output' '\n+\ttest_when_finished \"rm -rf porcelain-cfg porcelain-cli\" &&\n+\n+\t# Set up a bunch of references that we can use to demonstrate different\n+\t# kinds of flag symbols in the output format.\n+\tMAIN_OLD=$(git rev-parse HEAD) &&\n+\tgit branch \"fast-forward\" &&\n+\tgit branch \"deleted-branch\" &&\n+\tgit checkout -b force-updated &&\n+\ttest_commit --no-tag force-update-old &&\n+\tFORCE_UPDATED_OLD=$(git rev-parse HEAD) &&\n+\tgit checkout main &&\n+\n+\t# Clone and pre-seed the repositories. We fetch references into two\n+\t# namespaces so that we can test that rejected and force-updated\n+\t# references are reported properly.\n+\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n+\tgit clone . porcelain-cli &&\n+\tgit clone . porcelain-cfg &&\n+\tgit -C porcelain-cfg fetch origin $refspecs &&\n+\tgit -C porcelain-cli fetch origin $refspecs &&\n+\n+\t# Now that we have set up the client repositories we can change our\n+\t# local references.\n+\tgit branch new-branch &&\n+\tgit branch -d deleted-branch &&\n+\tgit checkout fast-forward &&\n+\ttest_commit --no-tag fast-forward-new &&\n+\tFAST_FORWARD_NEW=$(git rev-parse HEAD) &&\n+\tgit checkout force-updated &&\n+\tgit reset --hard HEAD~ &&\n+\ttest_commit --no-tag force-update-new &&\n+\tFORCE_UPDATED_NEW=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n+\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n+\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\tEOF\n+\n+\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n+\t# and non-dry-run fetches produces the same output. Execution of the\n+\t# fetch is expected to fail as we have a rejected reference update.\n+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --dry-run --prune origin $refspecs >actual-dry-run-cfg &&\n+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --dry-run --prune origin $refspecs >actual-dry-run-cli &&\n+\ttest_cmp actual-dry-run-cfg actual-dry-run-cli &&\n+\ttest_cmp expect actual-dry-run-cfg &&\n+\n+\t# And now we perform a non-dry-run fetch.\n+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --prune origin $refspecs >actual-cfg &&\n+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --prune origin $refspecs >actual-cli &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\ttest_cmp expect actual-cfg &&\n+\n+\t# Ensure that the dry-run and non-dry-run output matches.\n+\ttest_cmp actual-dry-run-cfg actual-cfg\n+'\n+\n test_expect_success 'fetch output with HEAD and --dry-run' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n@@ -99,6 +165,24 @@ test_expect_success 'fetch output with HEAD and --dry-run' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\tCOMMIT_ID=$(git rev-parse HEAD) &&\n+\n+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.0\n\n"},{"id":"475970","messageId":"6446e3db4702d_cd6129452@chronos.notmuch","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2023-04-24T20:17:31Z","receivedAt":"2023-04-24T20:17:40Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Patrick Steinhardt wrote:\n> Parsing the output of fetches is mostly impossible. It prettifies\n> reference names that are about to be updated, doesn't print the old and\n> new object IDs the refs are being updated from and to, and prints all of\n> that information in nice columns. In short, it is designed to be read by\n> humans rather than machines.\n> \n> This makes it hard to use in a script way though, e.g. to learn about\n> which references actually have been updated or which have not been\n> updated. This patch series intends to fix that by introducing a new\n> machine-parseable interface:\n> \n> ```\n> $ git fetch --output-format=porcelain --no-progress\n>   fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n> * 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n> * 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n> * 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n> * 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n> * 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n> ```\n\nMakes sense, my only question is what other format could `git fetch` have? I\nthink `--format=porcelain` is clear enough.\n\n-- \nFelipe Contreras\n"},{"id":"476018","messageId":"ZEekRNuPmObU9Vsq@ncase","threadId":"59619","inReplyTo":"6446e3db4702d_cd6129452@chronos.notmuch","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-25T09:58:28Z","receivedAt":"2023-04-25T09:58:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Apr 24, 2023 at 02:17:31PM -0600, Felipe Contreras wrote:\n> Patrick Steinhardt wrote:\n> > Parsing the output of fetches is mostly impossible. It prettifies\n> > reference names that are about to be updated, doesn't print the old and\n> > new object IDs the refs are being updated from and to, and prints all of\n> > that information in nice columns. In short, it is designed to be read by\n> > humans rather than machines.\n> > \n> > This makes it hard to use in a script way though, e.g. to learn about\n> > which references actually have been updated or which have not been\n> > updated. This patch series intends to fix that by introducing a new\n> > machine-parseable interface:\n> > \n> > ```\n> > $ git fetch --output-format=porcelain --no-progress\n> >   fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n> > * 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n> > * 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n> > * 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n> > * 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n> > * 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n> > ```\n> \n> Makes sense, my only question is what other format could `git fetch` have? I\n> think `--format=porcelain` is clear enough.\n\nYeah, I'd be perfectly happy to rename this to `--format=porcelain`.\nI'll wait for the Review Club that discusses this patch set tomorrow and\nwill send a new version with that change afterwards if nobody disagrees.\n\nPatrick\n"},{"id":"476131","messageId":"kl6l8reeo4no.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-26T18:54:35Z","receivedAt":"2023-04-26T18:54:41Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Hi Patrick!\n\nThanks for the pleasant read! I thought this was a great topic for\nReview Club. It's too bad that we missed you, but we post all relevant\nfeedback here anyway.\n\nNevertheless, if you'd like to see the meeting notes, you can find them\nat:\n\nhttps://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit\n\nPatrick Steinhardt <ps@pks.im> writes:\n\n> Parsing the output of fetches is mostly impossible. It prettifies\n> reference names that are about to be updated, doesn't print the old and\n> new object IDs the refs are being updated from and to, and prints all of\n> that information in nice columns. In short, it is designed to be read by\n> humans rather than machines.\n>\n> This makes it hard to use in a script way though, e.g. to learn about\n> which references actually have been updated or which have not been\n> updated. This patch series intends to fix that by introducing a new\n> machine-parseable interface:\n>\n> ```\n> $ git fetch --output-format=porcelain --no-progress\n>   fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n> * 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n> * 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n> * 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n> * 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n> * 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n> ```\n\nHaving machine-parseable output seems like an obviously good goal to me.\nThe finer points of the interface and output format are worth\ndiscussing. I'll do so in the patches themselves.\n\nOverall, I found the series very well-structured and easy to follow\nalong. Thanks.\n"},{"id":"476133","messageId":"b8225865-7eea-3dcb-247f-10cb22899b2a@intel.com","threadId":"59619","inReplyTo":"ZEekRNuPmObU9Vsq@ncase","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-26T19:14:57Z","receivedAt":"2023-04-26T19:15:26Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/25/2023 2:58 AM, Patrick Steinhardt wrote:\n> On Mon, Apr 24, 2023 at 02:17:31PM -0600, Felipe Contreras wrote:\n>> Patrick Steinhardt wrote:\n>>> Parsing the output of fetches is mostly impossible. It prettifies\n>>> reference names that are about to be updated, doesn't print the old and\n>>> new object IDs the refs are being updated from and to, and prints all of\n>>> that information in nice columns. In short, it is designed to be read by\n>>> humans rather than machines.\n>>>\n>>> This makes it hard to use in a script way though, e.g. to learn about\n>>> which references actually have been updated or which have not been\n>>> updated. This patch series intends to fix that by introducing a new\n>>> machine-parseable interface:\n>>>\n>>> ```\n>>> $ git fetch --output-format=porcelain --no-progress\n>>>   fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n>>> * 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n>>> * 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n>>> * 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n>>> * 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n>>> * 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n>>> ```\n>>\n>> Makes sense, my only question is what other format could `git fetch` have? I\n>> think `--format=porcelain` is clear enough.\n> \n> Yeah, I'd be perfectly happy to rename this to `--format=porcelain`.\n> I'll wait for the Review Club that discusses this patch set tomorrow and\n> will send a new version with that change afterwards if nobody disagrees.\n> \n> Patrick\n\nWe had some discussion during review club about this, where the idea of\nusing \"--porcelain\" came up because many commands use that when\nswitching into a machine readable format.\n\nIn addition, this format not only changes the output but also moves it\nfrom being on stderr to stdout, which is a hint that the intended usage\nof the command is now a little different.\n\nI don't have strong opinion here but want to note that \"--format\" is\noften used by commands like log which changes how we structure the\noutput of git objects.\n\nObviously using \"--porcelain\" is a bit weird when dealing with the\npre-existing compact and full outputs, and perhaps --format wouldn't be\nconfusing.\n\nI didn't find any command which used --output-format today, and all the\nuses of --format I saw were for object formatting like git log.\n\nI'm ok with --format, but wanted to note the potential confusion.\n\ngit status also uses --porcelain and has a -z option for NUL terminating\ninstead of newline terminating. We thought -z might be useful to handle\nthe potential of weird refnames that include characters. No one on the\nreview could remember what rules were enforced on refnames to confirm if\nit was legal to have '\\n' in a refname or not.\n"},{"id":"476134","messageId":"298a1063-d4e3-6cd8-cbde-cfce9e70df86@intel.com","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-26T19:17:16Z","receivedAt":"2023-04-26T19:17:25Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/19/2023 5:31 AM, Patrick Steinhardt wrote:\n> \n> ```\n> $ git fetch --output-format=porcelain --no-progress\n>   fff5a5e7f528b2ed2c335991399a766c2cf01103 af67688dca57999fd848f051eeea1d375ba546b2 refs/remotes/origin/master\n> * 0000000000000000000000000000000000000000 e046fe5a36a970bc14fbfbcb2074a48776f6b671 refs/remotes/origin/x86-rep-insns\n> * 0000000000000000000000000000000000000000 bb81ed6862b864c9eb99447f04d49a84ecb647e5 refs/tags/v6.3-rc4\n> * 0000000000000000000000000000000000000000 83af7b1468c0dca86b4dc9e43e73bfa4f38d9637 refs/tags/v6.3-rc5\n> * 0000000000000000000000000000000000000000 ab3affb8ed84f68638162fe7e6fd4055e15bff5b refs/tags/v6.3-rc6\n> * 0000000000000000000000000000000000000000 1c8c28415e8743368a2b800520a6dd0b22ee6ec2 refs/tags/v6.3-rc7\n> ```\n> \n\nOne thing that the standard output (maybe with --progress?) shows to\nstderr is which remote is being fetched and what URL was fetched.\n\nThis seems like useful data to include in a machine readable format. It\nwasn't clear if such output would still be made and whether it would go\nto stderr or stdout, nor whether that existing output was machine readable.\n\n\nObviously you can somewhat infer remotes based on the refs/remotes, but\nthat doesn't include other refs like tags.\n\nWhen fetching from multiple reports (--all or explicitly asking), I\nwonder if it makes sense to include some lines to differentiate where\neach block of updates for a given remote starts and ends?\n\nThanks,\nJake\n"},{"id":"476135","messageId":"8e08ac5f-b1cb-554c-dc21-4cbd93c348e8@intel.com","threadId":"59619","inReplyTo":"596e12f03a296d753ee6fe8face9522edc9e397e.1681906948.git.ps@pks.im","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-26T19:20:15Z","receivedAt":"2023-04-26T19:20:24Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/19/2023 5:31 AM, Patrick Steinhardt wrote:\n> When displaying reference updates, we print a line that looks similar to\n> the following:\n> \n> ```\n>  * branch               master          -> master\n> ```\n> \n> The \"branch\" bit changes depending on what kind of reference we're\n> updating, while both of the right-hand references are computed by\n> stripping well-known prefixes like \"refs/heads/\" or \"refs/tags\".\n> \n> The logic is kind of intertwined though and not easy to follow: we\n> precompute both the kind (e.g. \"branch\") and the what, which is the\n> abbreviated remote reference name, in `store_updated_refs()` and then\n> pass it down the call chain to `display_ref_update()`.\n> \n> There is a set of different cases here:\n> \n>     - When the remote reference name is \"HEAD\" we assume no kind and\n>       will thus instead print \"[new ref]\". We keep what at the empty\n>       string.\n> \n>     - When the remote reference name has a well-known prefix then the\n>       kind would be \"branch\", \"tag\" or \"remote-tracking branch\". The\n>       what is the reference with the well-known prefix stripped and in\n>       fact matches the output that `prettify_refname()` would return.\n> \n>     - Otherwise, we'll again assume no kind and keep the what set to the\n>       fully qualified reference name.\n> \n> Now there is a bug with the first case here, where the remote reference\n> name is \"HEAD\". As noted, \"what\" will be set to the empty string. And\n> that seems to be intentional because we also use this information to\n> update the FETCH_HEAD, and in case we're updating HEAD we seemingly\n> don't want to append that to our FETCH_HEAD value.\n> \n> But as mentioned, we also use this value to display reference updates.\n> And while the call to `display_ref_update()` correctly figures out that\n> we meant \"HEAD\" when `what` is empty, the call to `update_local_ref()`\n> doesn't. `update_local_ref()` will then call `display_ref_update()` with\n> the empty string and cause the following broken output:\n> \n> ```\n> $ git fetch --dry-run origin HEAD:foo\n> From https://github.com/git/git\n>  * [new ref]                          -> foo\n> ```\n> \n> The HEAD string is clearly missing from the left-hand side of the arrow,\n> which is further stressed by the point that the following commands work\n> as expected:\n> \n> ```\n> $ git fetch --dry-run origin HEAD\n> From https://github.com/git/git\n>  * branch                  HEAD       -> FETCH_HEAD\n> \n> $ git fetch --dry-run origin master\n> From https://github.com/git/git\n>  * branch                  master     -> FETCH_HEAD\n>  * branch                  master     -> origin/master\n> ```\n> \n> Fix this bug by instead unconditionally passing the full reference name\n> to `display_ref_update()` which learns to call `prettify_refname()` on\n> it. This does fix the above bug and is otherwise functionally the same\n> as `prettify_refname()` would only ever strip the well-known prefixes\n> just as intended. So at the same time, this also simplifies the code a\n> bit.\n> \n> Note that this patch also changes formatting of the block that computes\n> the \"kind\" and \"what\" variables. This is done on purpose so that it is\n> part of the diff, hopefully making the change easier to comprehend.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n\nThe commit message here has a lot of context, but I found it a bit hard\nto parse through, especially relative to the actual fix in code.\n\nOne suggestion was to load the paragraphs a bit more with the actual\nproblem being solved first, before beginning a lot of the context.\n\nWe also discussed the block of format changes and felt a bit mixed on\nwhether to include it or not. It does match the coding style guidelines,\nbut there is no actual functional change made to those lines in this series.\n\nI think its a good improvement, and it does force some extra context\ninto the diff which makes reading the resulting change easier.\n"},{"id":"476136","messageId":"36931566-1258-c5de-2efb-63541a2ce96c@intel.com","threadId":"59619","inReplyTo":"596e12f03a296d753ee6fe8face9522edc9e397e.1681906948.git.ps@pks.im","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-26T19:21:40Z","receivedAt":"2023-04-26T19:22:24Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/19/2023 5:31 AM, Patrick Steinhardt wrote:\n>  \n> +test_expect_success 'fetch output with HEAD and --dry-run' '\n> +\ttest_when_finished \"rm -rf head\" &&\n> +\tgit clone . head &&\n> +\n> +\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n> +\tcat >expect <<-EOF &&\n> +\tFrom $(test-tool path-utils real_path .)/.\n> +\t * branch            HEAD       -> FETCH_HEAD\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n> +\tcat >expect <<-EOF &&\n> +\tFrom $(test-tool path-utils real_path .)/.\n> +\t * [new ref]         HEAD       -> foo\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n\nThe test mentions HEAD and --dry-run, but the bug seems to exist\nregardless of whether --dry-run is used. I understand the use of\n--dry-run for testing fetch output so that you can repeatably run git\nfetch and get the same results.\n\nThe tests here should probably also have a test that covers fetch\nwithout --dry-run though.\n\nThanks,\nJake\n"},{"id":"476137","messageId":"kl6l5y9io37i.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"596e12f03a296d753ee6fe8face9522edc9e397e.1681906948.git.ps@pks.im","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-26T19:25:53Z","receivedAt":"2023-04-26T19:26:02Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Rearranging the lines slightly,\n\nPatrick Steinhardt <ps@pks.im> writes:\n\n> When displaying reference updates, we print a line that looks similar to\n> the following:\n>\n> ```\n>  * branch               master          -> master\n> ```\n>\n> The \"branch\" bit changes depending on what kind of reference we're\n> updating, while both of the right-hand references are computed by\n> stripping well-known prefixes like \"refs/heads/\" or \"refs/tags\".\n>\n> [...]\n>                   we also use this value to display reference updates.\n> And while the call to `display_ref_update()` correctly figures out that\n> we meant \"HEAD\" when `what` is empty, the call to `update_local_ref()`\n> doesn't. `update_local_ref()` will then call `display_ref_update()` with\n> the empty string and cause the following broken output:\n>\n> ```\n> $ git fetch --dry-run origin HEAD:foo\n> From https://github.com/git/git\n>  * [new ref]                          -> foo\n> ```\n>\n> [...]\n>\n> Fix this bug by instead unconditionally passing the full reference name\n> to `display_ref_update()` which learns to call `prettify_refname()` on\n> it. This does fix the above bug and is otherwise functionally the same\n> as `prettify_refname()` would only ever strip the well-known prefixes\n> just as intended. So at the same time, this also simplifies the code a\n> bit.\n\n\nThe bug fix is obviously good. I'm surprised we hadn't caught this\nsooner.\n\nAs a nitpicky comment, the commit message goes into a lot of detail,\nwhich makes it tricky to read on its own (though the level of detail\nmakes it easy to match to the diff, making the diff quite easy to\nfollow). I would have found this easier to read by summarizing the\nhigh-level mental model before diving into the background, e.g.\n\n\n  store_updated_refs() parses the remote ref name to create a 'note' to\n  write to FETCH_HEAD. This note is usually the prettified ref name, so\n  it is used to diplay ref updates (display_ref_update()). But if the\n  remote ref is HEAD, the note is the empty string [insert bug\n  description]. Instead, use the note only as a note and have\n  display_ref_update() prettify the ref name itself...\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index c310d89878..7c64f0c562 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n>  \t}\n>  \n>  \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n> +\tremote = prettify_refname(remote);\n> +\tlocal = prettify_refname(local);\n>  \n>  \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n>  \tif (!display_state->compact_format)\n> -\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n> +\t\tprint_remote_to_local(display_state, remote, local);\n>  \telse\n> -\t\tprint_compact(display_state, remote, prettify_refname(local));\n> +\t\tprint_compact(display_state, remote, local);\n>  \tif (error)\n>  \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n>  \tstrbuf_addch(&display_state->buf, '\\n');\n\nAs expected, we now prettify the name isntead of trusting the 'note'\nthat came in the parameter...\n\n> @@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n>  static int update_local_ref(struct ref *ref,\n>  \t\t\t    struct ref_transaction *transaction,\n>  \t\t\t    struct display_state *display_state,\n> -\t\t\t    const char *remote, const struct ref *remote_ref,\n> +\t\t\t    const struct ref *remote_ref,\n>  \t\t\t    int summary_width)\n>  {\n>  \tstruct commit *current = NULL, *updated;\n> @@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n>  \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n>  \t\tif (verbosity > 0)\n>  \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n> -\t\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\treturn 0;\n>  \t}\n>  \n> @@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n>  \t\t */\n>  \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n>  \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n> -\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\treturn 1;\n>  \t}\n>  \n> @@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n>  \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n>  \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n>  \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\t\treturn r;\n>  \t\t} else {\n>  \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n>  \t\t\t\t\t   _(\"would clobber existing tag\"),\n> -\t\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\t\treturn 1;\n>  \t\t}\n>  \t}\n> @@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n>  \t\tr = s_update_ref(msg, ref, transaction, 0);\n>  \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n>  \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\treturn r;\n>  \t}\n>  \n> @@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n>  \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n>  \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n>  \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n> -\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\tstrbuf_release(&quickref);\n>  \t\treturn r;\n>  \t} else if (force || ref->force) {\n> @@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n>  \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n>  \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n>  \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n> -\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\tstrbuf_release(&quickref);\n>  \t\treturn r;\n>  \t} else {\n>  \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n> -\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\treturn 1;\n>  \t}\n>  }\n\n...\n\n> @@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\t\t\t  display_state->url_len);\n>  \n>  \t\t\tif (ref) {\n> -\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n> +\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n>  \t\t\t\t\t\t       rm, summary_width);\n>  \t\t\t\tfree(ref);\n>  \t\t\t} else if (write_fetch_head || dry_run) {\n> @@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\t\t */\n>  \t\t\t\tdisplay_ref_update(display_state, '*',\n>  \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n> -\t\t\t\t\t\t   *what ? what : \"HEAD\",\n> +\t\t\t\t\t\t   rm->name,\n>  \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n>  \t\t\t}\n>  \t\t}\n\nand we stop passing the 'note' as a parameter. Looks good.\n\n> @@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n>  \t\t\t\tkind = \"\";\n>  \t\t\t\twhat = \"\";\n> -\t\t\t}\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n>  \t\t\t\tkind = \"branch\";\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n>  \t\t\t\tkind = \"tag\";\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n>  \t\t\t\tkind = \"remote-tracking branch\";\n> -\t\t\telse {\n> +\t\t\t} else {\n>  \t\t\t\tkind = \"\";\n>  \t\t\t\twhat = rm->name;\n>  \t\t\t}\n\nI really appreciate that this makes the patch easier to read. I don't\nreally appreciate this sort of churn, but it _is_ following\nCodingGuidelines:\n\n\t- When there are multiple arms to a conditional and some of them\n\t  require braces, enclose even a single line block in braces for\n\t  consistency. E.g.:\n\n\t\tif (foo) {\n\t\t\tdoit();\n\t\t} else {\n\t\t\tone();\n\t\t\ttwo();\n\t\t\tthree();\n\t\t}\n\n(I initialy thought it wasn't. Thanks to other Review Club participants\nfor pointing this out).\n\n> diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> index 0e45c27007..55f0f05b6a 100755\n> --- a/t/t5574-fetch-output.sh\n> +++ b/t/t5574-fetch-output.sh\n> @@ -54,6 +54,25 @@ test_expect_success 'fetch compact output' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'fetch output with HEAD and --dry-run' '\n\nThe commit message and diff didn't imply that this is a --dry-run only\nbug. I tested locally, and it seems to reproduce without --dry-run too,\nso I think we should drop \"--dry-run\" from this test name. In a later\npatch, you also add a test for porcelain output with --dry-run, but\nsince this test seems designed for just this bug, I think we can drop\nthe later test.\n"},{"id":"476138","messageId":"kl6l1qk6o2jm.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"3b2cad066a3b3446fc335d6944a62bf79b0779bb.1681906949.git.ps@pks.im","subject":"Re: [PATCH 7/8] fetch: introduce new `--output-format` option","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-26T19:40:13Z","receivedAt":"2023-04-26T19:40:19Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> @@ -2101,6 +2116,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n>  \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n>  \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n>  \t\t\t N_(\"dry run\")),\n> +\t\tOPT_CALLBACK(0, \"output-format\", &display_format, N_(\"format\"), N_(\"output format\"),\n> +\t\t\t     opt_parse_output_format),\n>  \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n>  \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n>  \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n\nThis change is good enough for fetching from a single remote, but if we\nwant to support \"--all\", we'd also need to propagate the CLI flag to the\nchild \"fetch\" processes. (The config option wouldn't have this bug\nbecause the child processes would parse config and get the correct\nvalue.)\n"},{"id":"476139","messageId":"kl6lzg6umne9.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"301138da039451519c6e60d6126e7756a54d346f.1681906949.git.ps@pks.im","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-26T19:52:46Z","receivedAt":"2023-04-26T19:52:52Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The output format is quite simple:\n>\n> ```\n> <flag> <old-object-id> <new-object-id> <local-reference>\\n\n> ```\n\nThis format doesn't show the remote name or url that was fetched. That\nseems okay when fetching with a single remote, but it seems necessary\nwith \"--all\". Perhaps you were planning to add that in a later series?\nIf so, I think it's okay to call the \"porcelain\" format experimental,\nand forbid porcelain + --all until then.\n\n>\n> We assume two conditions which are generally true:\n>\n>     - The old and new object IDs have fixed known widths and cannot\n>       contain spaces.\n>\n>     - References cannot contain newlines.\n\nThis seems like a non-issue if we add a -z CLI option to indicate that\nentries should be NUL terminated instead of newline terminated, but that\ncan be done as a followup.\n\n> With these assumptions, the output format becomes unambiguously\n> parseable. Furthermore, given that this output is designed to be\n> consumed by scripts, the machine-readable data is printed to stdout\n> instead of stderr like the human-readable output is. This is mostly done\n> so that other data printed to stderr, like error messages or progress\n> meters, don't interfere with the parseable data.\n\nSending the 'main output' to stdout makes sense to me, but this (and\npossibly respecting -z) sounds like a different mode of operation, not\njust a matter of formats. It seems different enough that I'd prefer not\nto piggyback on \"fetch.output\" for this (even though this adds more\nsurface to the interface...).\n\nWe could add --porcelain and say that \"fetch.output\" is ignored if\n--porcelain is also given. That also eliminates the need for\n--output-format, I think.\n\nThe .c changes look good to me.\n\n> +test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n> +\ttest_when_finished \"rm -rf head\" &&\n> +\tgit clone . head &&\n> +\tCOMMIT_ID=$(git rev-parse HEAD) &&\n> +\n> +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n> +\tcat >expect <<-EOF &&\n> +\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n> +\tcat >expect <<-EOF &&\n> +\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nAs mentioned upthread, I think this test isn't needed because\n\"porcelain\" wouldn't run into the bug we are checking for anyway.\n"},{"id":"476140","messageId":"xmqqildis89b.fsf@gitster.g","threadId":"59619","inReplyTo":"b8225865-7eea-3dcb-247f-10cb22899b2a@intel.com","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-04-26T20:23:12Z","receivedAt":"2023-04-26T20:23:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jacob Keller <jacob.e.keller@intel.com> writes:\n\n>> Yeah, I'd be perfectly happy to rename this to `--format=porcelain`.\n>> I'll wait for the Review Club that discusses this patch set tomorrow and\n>> will send a new version with that change afterwards if nobody disagrees.\n>> \n>> Patrick\n>\n> We had some discussion during review club about this, where the idea of\n> using \"--porcelain\" came up because many commands use that when\n> switching into a machine readable format.\n>\n> In addition, this format not only changes the output but also moves it\n> from being on stderr to stdout, which is a hint that the intended usage\n> of the command is now a little different.\n\nA little different from what?  I do not think the answer would be\n\"other program's --porcelain mode\", as sending them to stdout would\nbe one of the things that make the output easier for programs to\nparse, so it does sound like very much in the same spirit as \"git\nstatus --porcelain\" where its output format gets tweaked to be more\nmachine friendly.\n\nThe output with \"--porcelain\" option enabled tend to be less human\nfriendly and the distinction between Porcelain (for humans) and\nplumbing (for scripts) is reversed in the use of the word there---it\nstarted as \"this is the option for those who write Porcelain\ncommands to use\", but still it is not a very good name for the\noption.\n\nI am perfectly OK if the plan is to uniformly use --output-format\n(or something equally more descriptive) and migrate and deprecate\nthe \"--porcelain\" option away from existing commands.\n\nThanks.\n"},{"id":"476141","messageId":"xmqqa5yus879.fsf@gitster.g","threadId":"59619","inReplyTo":"b8225865-7eea-3dcb-247f-10cb22899b2a@intel.com","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-04-26T20:24:26Z","receivedAt":"2023-04-26T20:24:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jacob Keller <jacob.e.keller@intel.com> writes:\n\n>> Yeah, I'd be perfectly happy to rename this to `--format=porcelain`.\n>> I'll wait for the Review Club that discusses this patch set tomorrow and\n>> will send a new version with that change afterwards if nobody disagrees.\n>> \n>> Patrick\n>\n> We had some discussion during review club about this, where the idea of\n> using \"--porcelain\" came up because many commands use that when\n> switching into a machine readable format.\n>\n> In addition, this format not only changes the output but also moves it\n> from being on stderr to stdout, which is a hint that the intended usage\n> of the command is now a little different.\n\nA little different from what?  I do not think the answer would be\n\"other program's --porcelain mode\", as sending them to stdout would\nbe one of the things that make the output easier for programs to\nparse, so it does sound like very much in the same spirit as \"git\nstatus --porcelain\" where its output format gets tweaked to be more\nmachine friendly.\n\nThe output with \"--porcelain\" option enabled tend to be less human\nfriendly and the distinction between Porcelain (for humans) and\nplumbing (for scripts) is reversed in the use of the word there---it\nstarted as \"this is the option for those who write Porcelain\ncommands to use\", but still it is not a very good name for the\noption.\n\nI am perfectly OK if the plan is to uniformly use --output-format\n(or something equally more descriptive) and migrate and deprecate\nthe \"--porcelain\" option away from existing commands.\n\nThanks.\n"},{"id":"476142","messageId":"36378549-be98-1c76-7e4d-15e1a9123f12@intel.com","threadId":"59619","inReplyTo":"xmqqildis89b.fsf@gitster.g","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-26T20:30:02Z","receivedAt":"2023-04-26T20:30:32Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/26/2023 1:23 PM, Junio C Hamano wrote:\n> Jacob Keller <jacob.e.keller@intel.com> writes:\n> \n>>> Yeah, I'd be perfectly happy to rename this to `--format=porcelain`.\n>>> I'll wait for the Review Club that discusses this patch set tomorrow and\n>>> will send a new version with that change afterwards if nobody disagrees.\n>>>\n>>> Patrick\n>>\n>> We had some discussion during review club about this, where the idea of\n>> using \"--porcelain\" came up because many commands use that when\n>> switching into a machine readable format.\n>>\n>> In addition, this format not only changes the output but also moves it\n>> from being on stderr to stdout, which is a hint that the intended usage\n>> of the command is now a little different.\n> \n> A little different from what?  I do not think the answer would be\n> \"other program's --porcelain mode\", as sending them to stdout would\n> be one of the things that make the output easier for programs to\n> parse, so it does sound like very much in the same spirit as \"git\n> status --porcelain\" where its output format gets tweaked to be more\n> machine friendly.\n\nA little different from using git fetch normally where all output is\nstderr and is generally \"this is what I did\" but which was obviously not\nintended to be parsed by scripts but instead by the human who ran the\ncommand.\n\n> \n> The output with \"--porcelain\" option enabled tend to be less human\n> friendly and the distinction between Porcelain (for humans) and\n> plumbing (for scripts) is reversed in the use of the word there---it\n> started as \"this is the option for those who write Porcelain\n> commands to use\", but still it is not a very good name for the\n> option.\n\nYea the option sort of means \"to be used by those implementing\nporcelain\" and its definitely a bit confusing.\n\n> \n> I am perfectly OK if the plan is to uniformly use --output-format\n> (or something equally more descriptive) and migrate and deprecate\n> the \"--porcelain\" option away from existing commands.\n> \n> Thanks.\n\nThat makes sense to me as well.\n"},{"id":"476151","messageId":"kl6lttx2mjmb.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"kl6l8reeo4no.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-26T21:14:20Z","receivedAt":"2023-04-26T21:14:36Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Hi Patrick!\n>\n> Thanks for the pleasant read! I thought this was a great topic for\n> Review Club. It's too bad that we missed you, but we post all relevant\n> feedback here anyway.\n>\n> Nevertheless, if you'd like to see the meeting notes, you can find them\n> at:\n>\n> https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit\n\nIt seems like Jacob Keller and I have mostly the same comments (since we\ndid both go to the same Review Club, after all :)), so feel free to\nrespond to only Jacob's messages and ignore mine (where the messages\noverlap) - I'll catch up on those conversations.\n"},{"id":"476188","messageId":"ZEpVT-QjzYrmqgw-@ncase","threadId":"59619","inReplyTo":"kl6l1qk6o2jm.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:23Z","receivedAt":"2023-04-27T10:58:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 12:40:13PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > @@ -2101,6 +2116,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n> >  \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n> >  \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n> >  \t\t\t N_(\"dry run\")),\n> > +\t\tOPT_CALLBACK(0, \"output-format\", &display_format, N_(\"format\"), N_(\"output format\"),\n> > +\t\t\t     opt_parse_output_format),\n> >  \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n> >  \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n> >  \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n> \n> This change is good enough for fetching from a single remote, but if we\n> want to support \"--all\", we'd also need to propagate the CLI flag to the\n> child \"fetch\" processes. (The config option wouldn't have this bug\n> because the child processes would parse config and get the correct\n> value.)\n\nOh, right, good catch. Will fix, thanks!\n\nPatrick\n"},{"id":"476189","messageId":"ZEpVSrz-uUcfN_3_@ncase","threadId":"59619","inReplyTo":"kl6lzg6umne9.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:18Z","receivedAt":"2023-04-27T10:58:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 12:52:46PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The output format is quite simple:\n> >\n> > ```\n> > <flag> <old-object-id> <new-object-id> <local-reference>\\n\n> > ```\n> \n> This format doesn't show the remote name or url that was fetched. That\n> seems okay when fetching with a single remote, but it seems necessary\n> with \"--all\". Perhaps you were planning to add that in a later series?\n> If so, I think it's okay to call the \"porcelain\" format experimental,\n> and forbid porcelain + --all until then.\n\nThe reason is mostly that I didn't find an output format that I really\nliked here. We'd basically have to repeat the remote URL for every\nsingle reference: just repeating it once per remote doesn't fly because\nwith `--parallel` the output could be intermingled. But doing that feels\nwasteful to me, so I bailed. I guess I'm also biased here because it\njust wouldn't be useful to myself.\n\nSo with that in mind, I'd like to continue ignoring this issue for now\nand just not report the remote that the ref came from. But I'd also\nargue that we don't have to restrict porcelain mode to single-remote\nfetches: it can still be useful to do multi-remote fetches even without\nthe information where a certain reference update comes from. So any kind\nof restriction would feel artificial to me here.\n\nFurthermore, I'd argue that it is not necessary to label the format as\nexperimental only because of this limitation. With the refactorings done\nin this and the preceding patch series it is easy to add a new format in\ncase there indeed is somebody that would have a usecase for this. The\n\"porcelain\" format should stay stable, and if we decide that we want to\nalso report the remote for each reference in a follow-up we can easily\nadd a \"porcelain-v2\" or \"porcelain-with-remote\" format.\n\nAs I said though: I'm clearly biased, so if you feel like my train of\nthough is simply me being lazy then I'd carve in and adapt.\n\n> > We assume two conditions which are generally true:\n> >\n> >     - The old and new object IDs have fixed known widths and cannot\n> >       contain spaces.\n> >\n> >     - References cannot contain newlines.\n> \n> This seems like a non-issue if we add a -z CLI option to indicate that\n> entries should be NUL terminated instead of newline terminated, but that\n> can be done as a followup.\n\nYeah, either via `-z` or a new porcelain output format. But both of\nthese conditions should generally be true anyway, so I don't see that\nthose should become a problem.\n\n> > With these assumptions, the output format becomes unambiguously\n> > parseable. Furthermore, given that this output is designed to be\n> > consumed by scripts, the machine-readable data is printed to stdout\n> > instead of stderr like the human-readable output is. This is mostly done\n> > so that other data printed to stderr, like error messages or progress\n> > meters, don't interfere with the parseable data.\n> \n> Sending the 'main output' to stdout makes sense to me, but this (and\n> possibly respecting -z) sounds like a different mode of operation, not\n> just a matter of formats. It seems different enough that I'd prefer not\n> to piggyback on \"fetch.output\" for this (even though this adds more\n> surface to the interface...).\n> \n> We could add --porcelain and say that \"fetch.output\" is ignored if\n> --porcelain is also given. That also eliminates the need for\n> --output-format, I think.\n\nI was thinking about this initially, as well. But ultimately I decided\nagainst this especially because of your second paragraph: we'd now need\nto think about precedence of options and mutual exclusion, and that to\nme feels like an interface that is less obvious than a single knob that\nworks as you'd expect.\n\n> The .c changes look good to me.\n> \n> > +test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n> > +\ttest_when_finished \"rm -rf head\" &&\n> > +\tgit clone . head &&\n> > +\tCOMMIT_ID=$(git rev-parse HEAD) &&\n> > +\n> > +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n> > +\tcat >expect <<-EOF &&\n> > +\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n> > +\tEOF\n> > +\ttest_cmp expect actual &&\n> > +\n> > +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n> > +\tcat >expect <<-EOF &&\n> > +\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n> > +\tEOF\n> > +\ttest_cmp expect actual\n> > +'\n> \n> As mentioned upthread, I think this test isn't needed because\n> \"porcelain\" wouldn't run into the bug we are checking for anyway.\n\nThe only reason that the other bug was able to survive for so long was\nthat we didn't have test coverage there. So I think it makes sense to\nexplicitly test this, too, also because it causes us to walk a different\ncode path.\n\nLast but not least: this test uncovered a segfault I had in a previous\nversion. So I'd rather keep it :)\n\nPatrick\n"},{"id":"476190","messageId":"ZEpVVVgFQyrGzHrl@ncase","threadId":"59619","inReplyTo":"8e08ac5f-b1cb-554c-dc21-4cbd93c348e8@intel.com","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:29Z","receivedAt":"2023-04-27T10:58:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 12:20:15PM -0700, Jacob Keller wrote:\n> On 4/19/2023 5:31 AM, Patrick Steinhardt wrote:\n[snip]\n> The commit message here has a lot of context, but I found it a bit hard\n> to parse through, especially relative to the actual fix in code.\n> \n> One suggestion was to load the paragraphs a bit more with the actual\n> problem being solved first, before beginning a lot of the context.\n> \n> We also discussed the block of format changes and felt a bit mixed on\n> whether to include it or not. It does match the coding style guidelines,\n> but there is no actual functional change made to those lines in this series.\n> \n> I think its a good improvement, and it does force some extra context\n> into the diff which makes reading the resulting change easier.\n\nI initially really struggled to explain to myself why my change actually\nworks, and that shows in the commit message. I've reworded it a bit and\ntook some of the suggested changes that Glen posted to hopefully make\nthis clearer.\n\nPatrick\n"},{"id":"476191","messageId":"ZEpVXmzf3h5w99nZ@ncase","threadId":"59619","inReplyTo":"36931566-1258-c5de-2efb-63541a2ce96c@intel.com","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:38Z","receivedAt":"2023-04-27T10:58:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 12:21:40PM -0700, Jacob Keller wrote:\n> \n> \n> On 4/19/2023 5:31 AM, Patrick Steinhardt wrote:\n> >  \n> > +test_expect_success 'fetch output with HEAD and --dry-run' '\n> > +\ttest_when_finished \"rm -rf head\" &&\n> > +\tgit clone . head &&\n> > +\n> > +\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n> > +\tcat >expect <<-EOF &&\n> > +\tFrom $(test-tool path-utils real_path .)/.\n> > +\t * branch            HEAD       -> FETCH_HEAD\n> > +\tEOF\n> > +\ttest_cmp expect actual &&\n> > +\n> > +\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n> > +\tcat >expect <<-EOF &&\n> > +\tFrom $(test-tool path-utils real_path .)/.\n> > +\t * [new ref]         HEAD       -> foo\n> > +\tEOF\n> > +\ttest_cmp expect actual\n> > +'\n> > +\n> \n> The test mentions HEAD and --dry-run, but the bug seems to exist\n> regardless of whether --dry-run is used. I understand the use of\n> --dry-run for testing fetch output so that you can repeatably run git\n> fetch and get the same results.\n> \n> The tests here should probably also have a test that covers fetch\n> without --dry-run though.\n\nMakes sense, will amend!\n\nPatrick\n"},{"id":"476192","messageId":"ZEpVZcvz_OWhno1O@ncase","threadId":"59619","inReplyTo":"kl6l5y9io37i.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:45Z","receivedAt":"2023-04-27T10:58:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 12:25:53PM -0700, Glen Choo wrote:\n> Rearranging the lines slightly,\n> \n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > When displaying reference updates, we print a line that looks similar to\n> > the following:\n> >\n> > ```\n> >  * branch               master          -> master\n> > ```\n> >\n> > The \"branch\" bit changes depending on what kind of reference we're\n> > updating, while both of the right-hand references are computed by\n> > stripping well-known prefixes like \"refs/heads/\" or \"refs/tags\".\n> >\n> > [...]\n> >                   we also use this value to display reference updates.\n> > And while the call to `display_ref_update()` correctly figures out that\n> > we meant \"HEAD\" when `what` is empty, the call to `update_local_ref()`\n> > doesn't. `update_local_ref()` will then call `display_ref_update()` with\n> > the empty string and cause the following broken output:\n> >\n> > ```\n> > $ git fetch --dry-run origin HEAD:foo\n> > From https://github.com/git/git\n> >  * [new ref]                          -> foo\n> > ```\n> >\n> > [...]\n> >\n> > Fix this bug by instead unconditionally passing the full reference name\n> > to `display_ref_update()` which learns to call `prettify_refname()` on\n> > it. This does fix the above bug and is otherwise functionally the same\n> > as `prettify_refname()` would only ever strip the well-known prefixes\n> > just as intended. So at the same time, this also simplifies the code a\n> > bit.\n> \n> \n> The bug fix is obviously good. I'm surprised we hadn't caught this\n> sooner.\n> \n> As a nitpicky comment, the commit message goes into a lot of detail,\n> which makes it tricky to read on its own (though the level of detail\n> makes it easy to match to the diff, making the diff quite easy to\n> follow). I would have found this easier to read by summarizing the\n> high-level mental model before diving into the background, e.g.\n> \n> \n>   store_updated_refs() parses the remote ref name to create a 'note' to\n>   write to FETCH_HEAD. This note is usually the prettified ref name, so\n>   it is used to diplay ref updates (display_ref_update()). But if the\n>   remote ref is HEAD, the note is the empty string [insert bug\n>   description]. Instead, use the note only as a note and have\n>   display_ref_update() prettify the ref name itself...\n\nI like that and will use a variant of this.\n\n[snip]\n> > diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> > index 0e45c27007..55f0f05b6a 100755\n> > --- a/t/t5574-fetch-output.sh\n> > +++ b/t/t5574-fetch-output.sh\n> > @@ -54,6 +54,25 @@ test_expect_success 'fetch compact output' '\n> >  \ttest_cmp expect actual\n> >  '\n> >  \n> > +test_expect_success 'fetch output with HEAD and --dry-run' '\n> \n> The commit message and diff didn't imply that this is a --dry-run only\n> bug. I tested locally, and it seems to reproduce without --dry-run too,\n> so I think we should drop \"--dry-run\" from this test name. In a later\n> patch, you also add a test for porcelain output with --dry-run, but\n> since this test seems designed for just this bug, I think we can drop\n> the later test.\n\nTrue, I'll amend the test.\n\nPatrick\n"},{"id":"476193","messageId":"ZEpVatqnhdUSMWYY@ncase","threadId":"59619","inReplyTo":"xmqqildis89b.fsf@gitster.g","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T10:58:50Z","receivedAt":"2023-04-27T10:59:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 26, 2023 at 01:23:12PM -0700, Junio C Hamano wrote:\n> Jacob Keller <jacob.e.keller@intel.com> writes:\n> \n> >> Yeah, I'd be perfectly happy to rename this to `--format=porcelain`.\n> >> I'll wait for the Review Club that discusses this patch set tomorrow and\n> >> will send a new version with that change afterwards if nobody disagrees.\n> >> \n> >> Patrick\n> >\n> > We had some discussion during review club about this, where the idea of\n> > using \"--porcelain\" came up because many commands use that when\n> > switching into a machine readable format.\n> >\n> > In addition, this format not only changes the output but also moves it\n> > from being on stderr to stdout, which is a hint that the intended usage\n> > of the command is now a little different.\n> \n> A little different from what?  I do not think the answer would be\n> \"other program's --porcelain mode\", as sending them to stdout would\n> be one of the things that make the output easier for programs to\n> parse, so it does sound like very much in the same spirit as \"git\n> status --porcelain\" where its output format gets tweaked to be more\n> machine friendly.\n> \n> The output with \"--porcelain\" option enabled tend to be less human\n> friendly and the distinction between Porcelain (for humans) and\n> plumbing (for scripts) is reversed in the use of the word there---it\n> started as \"this is the option for those who write Porcelain\n> commands to use\", but still it is not a very good name for the\n> option.\n\nAh, thanks for explaining where this reversed meaning actually comes\nfrom. I really wondered why we use \"porcelain\" in preexisting code to\nreflect machine-parseable interface, but that explanation does make\nsense.\n\n> I am perfectly OK if the plan is to uniformly use --output-format\n> (or something equally more descriptive) and migrate and deprecate\n> the \"--porcelain\" option away from existing commands.\n\nI'm still not quite clear on where the consensus lies now. Personally, I\nthink that both `--format` and `--output-format` work well and are a bit\nmore descriptive than simply `--porcelain`, and wouldn't mind also\nmigrating other binaries to use either of them.\n\nFurthermore, I think that `--[output-]format` has the advantage that you\ndon't need to handle priorities or mutual exclusion of different options\nthat all apply to the reference format. To a user, it is not immediately\nobvious what `git fetch --format=compact --porcelain` would do, and\nwhich of both options ultimately get respected. But that's likely only\ntrue for future commands, because any migration would create the same\nkind of ambiguity for preexisting commands.\n\nIf we were to also migrate preexisting code to use `--[output-]format`\nthen I'd argue that `--output-format` is likely the better name, mostly\nbecause it is less likely to be ambiguous compared to `--format`. The\nlatter could e.g. easily confused with `--object-format`.\n\nSo I think I'll stick with `--output-format` for the time being.\n\nPatrick\n"},{"id":"476194","messageId":"cover.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH v2 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:04Z","receivedAt":"2023-04-27T11:13:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the second version of my patch series that introduces a\nmachine-parseable output.\n\nChanges compared to v1:\n\n    - Patch 3/8: I've reworded the commit message to hopefully be more\n      straight-forward. I've also amended tests so that we don't only\n      test with `--dry-run`, but also without.\n\n    - Patch 6/8: I've also moved the `all` and `multiple` variables from\n      their global scope into `cmd__fetch`.\n\n    - Patch 7/8: Fixed a bug where `--output-format=` wasn't honored for\n      child processes when doing multi-remote fetches. Furthermore, I've\n      unified parsing of the actual format so that we don't have to\n      repeat it thrice.\n\n    - Patch 8/8: Added a note to the commit message that tries to argue\n      why I didn't add remote information to the interface. I'm still\n      open to change this if you disagree with my reasoning here.\n\nThanks a bunch for all the feedback received so far, really appreciate\nit!\n\nPatrick\n\nPatrick Steinhardt (8):\n  fetch: split out tests for output format\n  fetch: add a test to exercise invalid output formats\n  fetch: fix missing from-reference when fetching HEAD:foo\n  fetch: introduce `display_format` enum\n  fetch: move display format parsing into main function\n  fetch: move option related variables into main function\n  fetch: introduce new `--output-format` option\n  fetch: introduce machine-parseable \"porcelain\" output format\n\n Documentation/config/fetch.txt  |   4 +-\n Documentation/fetch-options.txt |   5 +\n Documentation/git-fetch.txt     |  17 +-\n builtin/fetch.c                 | 427 ++++++++++++++++++++------------\n t/t5510-fetch.sh                |  53 ----\n t/t5574-fetch-output.sh         | 237 ++++++++++++++++++\n 6 files changed, 521 insertions(+), 222 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\nRange-diff against v1:\n1:  0d0d50d14c = 1:  0d0d50d14c fetch: split out tests for output format\n2:  29d2c58914 = 2:  29d2c58914 fetch: add a test to exercise invalid output formats\n3:  596e12f03a ! 3:  d1fb6eeae7 fetch: fix missing from-reference when fetching HEAD:foo\n    @@ Metadata\n      ## Commit message ##\n         fetch: fix missing from-reference when fetching HEAD:foo\n     \n    -    When displaying reference updates, we print a line that looks similar to\n    -    the following:\n    +    `store_updated_refs()` parses the remote reference for two purposes:\n    +\n    +        - It gets used as a note when writing FETCH_HEAD.\n    +\n    +        - It is passed through to `display_ref_update()` to display\n    +          updated references in the following format:\n    +\n    +          ```\n    +           * branch               master          -> master\n    +          ```\n    +\n    +    In most cases, the parsed remote reference is the prettified reference\n    +    name and can thus be used for both cases. But if the remote reference is\n    +    HEAD, the parsed remote reference becomes empty. This is intended when\n    +    we write the FETCH_HEAD, where we skip writing the note in that case.\n    +    But it is not intended when displaying the updated references and would\n    +    cause us to miss the left-hand side of the displayed reference update:\n     \n         ```\n    -     * branch               master          -> master\n    -    ```\n    -\n    -    The \"branch\" bit changes depending on what kind of reference we're\n    -    updating, while both of the right-hand references are computed by\n    -    stripping well-known prefixes like \"refs/heads/\" or \"refs/tags\".\n    -\n    -    The logic is kind of intertwined though and not easy to follow: we\n    -    precompute both the kind (e.g. \"branch\") and the what, which is the\n    -    abbreviated remote reference name, in `store_updated_refs()` and then\n    -    pass it down the call chain to `display_ref_update()`.\n    -\n    -    There is a set of different cases here:\n    -\n    -        - When the remote reference name is \"HEAD\" we assume no kind and\n    -          will thus instead print \"[new ref]\". We keep what at the empty\n    -          string.\n    -\n    -        - When the remote reference name has a well-known prefix then the\n    -          kind would be \"branch\", \"tag\" or \"remote-tracking branch\". The\n    -          what is the reference with the well-known prefix stripped and in\n    -          fact matches the output that `prettify_refname()` would return.\n    -\n    -        - Otherwise, we'll again assume no kind and keep the what set to the\n    -          fully qualified reference name.\n    -\n    -    Now there is a bug with the first case here, where the remote reference\n    -    name is \"HEAD\". As noted, \"what\" will be set to the empty string. And\n    -    that seems to be intentional because we also use this information to\n    -    update the FETCH_HEAD, and in case we're updating HEAD we seemingly\n    -    don't want to append that to our FETCH_HEAD value.\n    -\n    -    But as mentioned, we also use this value to display reference updates.\n    -    And while the call to `display_ref_update()` correctly figures out that\n    -    we meant \"HEAD\" when `what` is empty, the call to `update_local_ref()`\n    -    doesn't. `update_local_ref()` will then call `display_ref_update()` with\n    -    the empty string and cause the following broken output:\n    -\n    -    ```\n    -    $ git fetch --dry-run origin HEAD:foo\n    +    $ git fetch origin HEAD:foo\n         From https://github.com/git/git\n          * [new ref]                          -> foo\n         ```\n     \n         The HEAD string is clearly missing from the left-hand side of the arrow,\n    -    which is further stressed by the point that the following commands work\n    -    as expected:\n    +    which is further stressed by the point that the following commands show\n    +    the left-hand side as expected:\n     \n         ```\n    -    $ git fetch --dry-run origin HEAD\n    +    $ git fetch origin HEAD\n         From https://github.com/git/git\n          * branch                  HEAD       -> FETCH_HEAD\n     \n    -    $ git fetch --dry-run origin master\n    +    $ git fetch origin master\n         From https://github.com/git/git\n          * branch                  master     -> FETCH_HEAD\n          * branch                  master     -> origin/master\n         ```\n     \n    -    Fix this bug by instead unconditionally passing the full reference name\n    -    to `display_ref_update()` which learns to call `prettify_refname()` on\n    -    it. This does fix the above bug and is otherwise functionally the same\n    -    as `prettify_refname()` would only ever strip the well-known prefixes\n    -    just as intended. So at the same time, this also simplifies the code a\n    -    bit.\n    +    The logic of how we compute the remote reference name that we ultimately\n    +    pass to `display_ref_update()` is not easy to follow. There are three\n    +    different cases here:\n    +\n    +        - When the remote reference name is \"HEAD\" we set the remote\n    +          reference name to the empty string. This is the case that causes\n    +          the bug to occur, where we would indeed want to print \"HEAD\"\n    +          instead of the empty string. This is what `prettify_refname()`\n    +          would return.\n    +\n    +        - When the remote reference name has a well-known prefix then we\n    +          strip this prefix. This matches what `prettify_refname()` does.\n    +\n    +        - Otherwise, we keep the fully qualified reference name. This also\n    +          matches what `prettify_refname()` does.\n    +\n    +    As the return value of `prettify_refname()` would do the correct thing\n    +    for us in all three cases, we can fix the bug by passing through the\n    +    full remote reference name to `display_ref_update()`, which learns to\n    +    call `prettify_refname()`. At the same time, this also simplifies the\n    +    code a bit.\n     \n         Note that this patch also changes formatting of the block that computes\n         the \"kind\" and \"what\" variables. This is done on purpose so that it is\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +\tEOF\n     +\ttest_cmp expect actual &&\n     +\n    ++\tgit -C head fetch origin HEAD >actual 2>&1 &&\n    ++\ttest_cmp expect actual &&\n    ++\n     +\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n     +\tcat >expect <<-EOF &&\n     +\tFrom $(test-tool path-utils real_path .)/.\n     +\t * [new ref]         HEAD       -> foo\n     +\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tgit -C head fetch origin HEAD:foo >actual 2>&1 &&\n     +\ttest_cmp expect actual\n     +'\n     +\n4:  8571363be1 = 4:  b545bf8bb9 fetch: introduce `display_format` enum\n5:  d98c3ee0ce = 5:  4990d35998 fetch: move display format parsing into main function\n6:  640a8840e1 ! 6:  cfe84129ab fetch: move option related variables into main function\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## builtin/fetch.c ##\n    -@@ builtin/fetch.c: static int all, append, dry_run, force, keep, multiple, update_head_ok;\n    +@@ builtin/fetch.c: static int fetch_prune_tags_config = -1; /* unspecified */\n    + static int prune_tags = -1; /* unspecified */\n    + #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */\n    + \n    +-static int all, append, dry_run, force, keep, multiple, update_head_ok;\n    ++static int append, dry_run, force, keep, update_head_ok;\n      static int write_fetch_head = 1;\n      static int verbosity, deepen_relative, set_upstream, refetch;\n      static int progress = -1;\n    @@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const cha\n      \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n      \tstruct string_list list = STRING_LIST_INIT_DUP;\n      \tstruct remote *remote = NULL;\n    ++\tint all = 0, multiple = 0;\n      \tint result = 0;\n      \tint prune_tags_ok = 1;\n     +\tint enable_auto_gc = 1;\n7:  3b2cad066a ! 7:  0335e5eeb4 fetch: introduce new `--output-format` option\n    @@ Commit message\n         current mechanism feels a little bit indirect and rigid.\n     \n         Introduce a new `--output-format` option that allows the user to change\n    -    the desired output format more directly.\n    +    the desired format more directly.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Documentation/fetch-options.txt: linkgit:git-config[1].\n      \tWrite the list of remote refs fetched in the `FETCH_HEAD`\n     \n      ## builtin/fetch.c ##\n    +@@ builtin/fetch.c: enum display_format {\n    + \tDISPLAY_FORMAT_UNKNOWN = 0,\n    + \tDISPLAY_FORMAT_FULL,\n    + \tDISPLAY_FORMAT_COMPACT,\n    ++\tDISPLAY_FORMAT_MAX,\n    ++};\n    ++\n    ++static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n    ++\tNULL,\n    ++\t\"full\",\n    ++\t\"compact\",\n    + };\n    + \n    + struct display_state {\n    +@@ builtin/fetch.c: static int fetch_finished(int result, struct strbuf *out,\n    + \treturn 0;\n    + }\n    + \n    +-static int fetch_multiple(struct string_list *list, int max_children)\n    ++static int fetch_multiple(struct string_list *list, int max_children,\n    ++\t\t\t  enum display_format format)\n    + {\n    + \tint i, result = 0;\n    + \tstruct strvec argv = STRVEC_INIT;\n    +@@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n    + \t\t     \"--no-write-commit-graph\", NULL);\n    + \tadd_options_to_argv(&argv);\n    + \n    ++\tif (format != DISPLAY_FORMAT_UNKNOWN)\n    ++\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n    ++\n    + \tif (max_children != 1 && list->nr != 1) {\n    + \t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n    + \t\tconst struct run_process_parallel_opts opts = {\n     @@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,\n      \treturn exit_code;\n      }\n      \n    ++static enum display_format parse_display_format(const char *format)\n    ++{\n    ++\tfor (int i = 0; i < ARRAY_SIZE(display_formats); i++)\n    ++\t\tif (display_formats[i] && !strcmp(display_formats[i], format))\n    ++\t\t\treturn i;\n    ++\treturn DISPLAY_FORMAT_UNKNOWN;\n    ++}\n    ++\n     +static int opt_parse_output_format(const struct option *opt, const char *arg, int unset)\n     +{\n    -+\tenum display_format *format = opt->value;\n    ++\tenum display_format *format = opt->value, parsed;\n    ++\n     +\tif (unset || !arg)\n     +\t\treturn 1;\n    -+\telse if (!strcmp(arg, \"full\"))\n    -+\t\t*format = DISPLAY_FORMAT_FULL;\n    -+\telse if (!strcmp(arg, \"compact\"))\n    -+\t\t*format = DISPLAY_FORMAT_COMPACT;\n    -+\telse\n    ++\n    ++\tparsed = parse_display_format(arg);\n    ++\tif (parsed == DISPLAY_FORMAT_UNKNOWN)\n     +\t\treturn error(_(\"unsupported output format '%s'\"), arg);\n    ++\t*format = parsed;\n     +\n     +\treturn 0;\n     +}\n    @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n      \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n      \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\tconst char *format = \"full\";\n    + \n    + \t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n    +-\t\tif (!strcasecmp(format, \"full\"))\n    +-\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n    +-\t\telse if (!strcasecmp(format, \"compact\"))\n    +-\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n    +-\t\telse\n    ++\n    ++\t\tdisplay_format = parse_display_format(format);\n    ++\t\tif (display_format == DISPLAY_FORMAT_UNKNOWN)\n    + \t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n    + \t\t\t    \"fetch.output\", format);\n    + \t}\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\t\tmax_children = fetch_parallel_config;\n    + \n    + \t\t/* TODO should this also die if we have a previous partial-clone? */\n    +-\t\tresult = fetch_multiple(&list, max_children);\n    ++\t\tresult = fetch_multiple(&list, max_children, display_format);\n    + \t}\n    + \n    + \n     \n      ## t/t5574-fetch-output.sh ##\n     @@ t/t5574-fetch-output.sh: test_expect_success 'fetch with invalid output format configuration' '\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch aligned output' '\n      \tcat >expect <<-\\EOF &&\n      \tmain       -> origin/*\n      \textraaa    -> *\n    +@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n    + \ttest_cmp expect actual\n    + '\n    + \n    ++test_expect_success 'fetch compact output with multiple remotes' '\n    ++\ttest_when_finished \"rm -rf compact-cfg compact-cli\" &&\n    ++\n    ++\tgit clone . compact-cli &&\n    ++\tgit -C compact-cli remote add second-remote \"$PWD\" &&\n    ++\tgit clone . compact-cfg &&\n    ++\tgit -C compact-cfg remote add second-remote \"$PWD\" &&\n    ++\ttest_commit multi-commit &&\n    ++\n    ++\tgit -C compact-cfg -c fetch.output=compact fetch --all >actual-cfg 2>&1 &&\n    ++\tgit -C compact-cli fetch --output-format=compact --all >actual-cli 2>&1 &&\n    ++\ttest_cmp actual-cfg actual-cli &&\n    ++\n    ++\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\tmain         -> origin/*\n    ++\tmulti-commit -> *\n    ++\tmain       -> second-remote/*\n    ++\tEOF\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    + test_expect_success 'fetch output with HEAD and --dry-run' '\n    + \ttest_when_finished \"rm -rf head\" &&\n    + \tgit clone . head &&\n8:  301138da03 ! 8:  d7c1bc1a80 fetch: introduce machine-parseable \"porcelain\" output format\n    @@ Commit message\n         so that other data printed to stderr, like error messages or progress\n         meters, don't interfere with the parseable data.\n     \n    +    A notable ommission here is that the output format does not include the\n    +    remote from which a reference was fetched, which might be important\n    +    information especially in the context of multi-remote fetches. But as\n    +    such a format would require us to print the remote for every single\n    +    reference update due to parallelizable fetches it feels wasteful for the\n    +    most likely usecase, which is when fetching from a single remote. If\n    +    usecases come up for this in the future though it is easy enough to add\n    +    a new \"porcelain-v2\" format that adds this information.\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/config/fetch.txt ##\n    @@ builtin/fetch.c: enum display_format {\n      \tDISPLAY_FORMAT_FULL,\n      \tDISPLAY_FORMAT_COMPACT,\n     +\tDISPLAY_FORMAT_PORCELAIN,\n    + \tDISPLAY_FORMAT_MAX,\n    + };\n    + \n    +@@ builtin/fetch.c: static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n    + \tNULL,\n    + \t\"full\",\n    + \t\"compact\",\n    ++\t\"porcelain\",\n      };\n      \n      struct display_state {\n    @@ builtin/fetch.c: static int prune_refs(struct display_state *display_state,\n      \t\t\t\t\t   summary_width);\n      \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n      \t\t}\n    -@@ builtin/fetch.c: static int opt_parse_output_format(const struct option *opt, const char *arg, in\n    - \t\t*format = DISPLAY_FORMAT_FULL;\n    - \telse if (!strcmp(arg, \"compact\"))\n    - \t\t*format = DISPLAY_FORMAT_COMPACT;\n    -+\telse if (!strcmp(arg, \"porcelain\"))\n    -+\t\t*format = DISPLAY_FORMAT_PORCELAIN;\n    - \telse\n    - \t\treturn error(_(\"unsupported output format '%s'\"), arg);\n    - \n    -@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    - \t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n    - \t\telse if (!strcasecmp(format, \"compact\"))\n    - \t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n    -+\t\telse if (!strcasecmp(format, \"porcelain\"))\n    -+\t\t\tdisplay_format = DISPLAY_FORMAT_PORCELAIN;\n    - \t\telse\n    - \t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n    - \t\t\t    \"fetch.output\", format);\n     \n      ## t/t5574-fetch-output.sh ##\n    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n    +@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output with multiple remotes' '\n      \ttest_cmp expect actual\n      '\n      \n-- \n2.40.1\n\n"},{"id":"476195","messageId":"0d0d50d14c557f8313747e7d0e104c2c0819dab9.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 1/8] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:08Z","receivedAt":"2023-04-27T11:13:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new porcelain mode for the output of\ngit-fetch(1). As part of that we'll be introducing a set of new tests\nthat only relate to the output of this command.\n\nSplit out tests that exercise the output format of git-fetch(1) so that\nit becomes easier to verify this functionality as a standalone unit. As\nthe tests assume that the default branch is called \"main\" we set up the\ncorresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\naccordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5510-fetch.sh        | 53 ----------------------------------\n t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 63 insertions(+), 53 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex dc44da9c79..4f289063ce 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1118,59 +1118,6 @@ test_expect_success 'fetching with auto-gc does not lock up' '\n \t)\n '\n \n-test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n-\ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n-\tlooooooooooooong-tag -> looooooooooooong-tag\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n-\ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n-\textraaa    -> *\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--no-show-forced-updates' '\n-\tmkdir forced-updates &&\n-\t(\n-\t\tcd forced-updates &&\n-\t\tgit init &&\n-\t\ttest_commit 1 &&\n-\t\ttest_commit 2\n-\t) &&\n-\tgit clone forced-updates forced-update-clone &&\n-\tgit clone forced-updates no-forced-update-clone &&\n-\tgit -C forced-updates reset --hard HEAD~1 &&\n-\t(\n-\t\tcd forced-update-clone &&\n-\t\tgit fetch --show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep \"(forced update)\" output\n-\t) &&\n-\t(\n-\t\tcd no-forced-update-clone &&\n-\t\tgit fetch --no-show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep ! \"(forced update)\" output\n-\t)\n-'\n-\n for section in fetch transfer\n do\n \ttest_expect_success \"$section.hideRefs affects connectivity check\" '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nnew file mode 100755\nindex 0000000000..f91b654d38\n--- /dev/null\n+++ b/t/t5574-fetch-output.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='git fetch output format'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'fetch aligned output' '\n+\tgit clone . full-output &&\n+\ttest_commit looooooooooooong-tag &&\n+\t(\n+\t\tcd full-output &&\n+\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain                 -> origin/main\n+\tlooooooooooooong-tag -> looooooooooooong-tag\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'fetch compact output' '\n+\tgit clone . compact &&\n+\ttest_commit extraaa &&\n+\t(\n+\t\tcd compact &&\n+\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain       -> origin/*\n+\textraaa    -> *\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-show-forced-updates' '\n+\tmkdir forced-updates &&\n+\t(\n+\t\tcd forced-updates &&\n+\t\tgit init &&\n+\t\ttest_commit 1 &&\n+\t\ttest_commit 2\n+\t) &&\n+\tgit clone forced-updates forced-update-clone &&\n+\tgit clone forced-updates no-forced-update-clone &&\n+\tgit -C forced-updates reset --hard HEAD~1 &&\n+\t(\n+\t\tcd forced-update-clone &&\n+\t\tgit fetch --show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep \"(forced update)\" output\n+\t) &&\n+\t(\n+\t\tcd no-forced-update-clone &&\n+\t\tgit fetch --no-show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep ! \"(forced update)\" output\n+\t)\n+'\n+\n+test_done\n-- \n2.40.1\n\n"},{"id":"476196","messageId":"29d2c58914b7270a4603cd65565c06ff74355e96.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 2/8] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:12Z","receivedAt":"2023-04-27T11:13:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add a testcase that exercises the logic when an invalid output format is\npassed via the `fetch.output` configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5574-fetch-output.sh | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex f91b654d38..0e45c27007 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -7,6 +7,23 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+test_expect_success 'fetch with invalid output format configuration' '\n+\ttest_when_finished \"rm -rf clone\" &&\n+\tgit clone . clone &&\n+\n+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch aligned output' '\n \tgit clone . full-output &&\n \ttest_commit looooooooooooong-tag &&\n-- \n2.40.1\n\n"},{"id":"476197","messageId":"d1fb6eeae76bc550030475e40868b90a2ca6ce8c.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:16Z","receivedAt":"2023-04-27T11:13:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"`store_updated_refs()` parses the remote reference for two purposes:\n\n    - It gets used as a note when writing FETCH_HEAD.\n\n    - It is passed through to `display_ref_update()` to display\n      updated references in the following format:\n\n      ```\n       * branch               master          -> master\n      ```\n\nIn most cases, the parsed remote reference is the prettified reference\nname and can thus be used for both cases. But if the remote reference is\nHEAD, the parsed remote reference becomes empty. This is intended when\nwe write the FETCH_HEAD, where we skip writing the note in that case.\nBut it is not intended when displaying the updated references and would\ncause us to miss the left-hand side of the displayed reference update:\n\n```\n$ git fetch origin HEAD:foo\nFrom https://github.com/git/git\n * [new ref]                          -> foo\n```\n\nThe HEAD string is clearly missing from the left-hand side of the arrow,\nwhich is further stressed by the point that the following commands show\nthe left-hand side as expected:\n\n```\n$ git fetch origin HEAD\nFrom https://github.com/git/git\n * branch                  HEAD       -> FETCH_HEAD\n\n$ git fetch origin master\nFrom https://github.com/git/git\n * branch                  master     -> FETCH_HEAD\n * branch                  master     -> origin/master\n```\n\nThe logic of how we compute the remote reference name that we ultimately\npass to `display_ref_update()` is not easy to follow. There are three\ndifferent cases here:\n\n    - When the remote reference name is \"HEAD\" we set the remote\n      reference name to the empty string. This is the case that causes\n      the bug to occur, where we would indeed want to print \"HEAD\"\n      instead of the empty string. This is what `prettify_refname()`\n      would return.\n\n    - When the remote reference name has a well-known prefix then we\n      strip this prefix. This matches what `prettify_refname()` does.\n\n    - Otherwise, we keep the fully qualified reference name. This also\n      matches what `prettify_refname()` does.\n\nAs the return value of `prettify_refname()` would do the correct thing\nfor us in all three cases, we can fix the bug by passing through the\nfull remote reference name to `display_ref_update()`, which learns to\ncall `prettify_refname()`. At the same time, this also simplifies the\ncode a bit.\n\nNote that this patch also changes formatting of the block that computes\nthe \"kind\" and \"what\" variables. This is done on purpose so that it is\npart of the diff, hopefully making the change easier to comprehend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c         | 37 +++++++++++++++++++------------------\n t/t5574-fetch-output.sh | 25 +++++++++++++++++++++++++\n 2 files changed, 44 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c310d89878..7c64f0c562 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t}\n \n \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\tremote = prettify_refname(remote);\n+\tlocal = prettify_refname(local);\n \n \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n \tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n+\t\tprint_remote_to_local(display_state, remote, local);\n \telse\n-\t\tprint_compact(display_state, remote, prettify_refname(local));\n+\t\tprint_compact(display_state, remote, local);\n \tif (error)\n \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n \tstrbuf_addch(&display_state->buf, '\\n');\n@@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n-\t\t\t    const char *remote, const struct ref *remote_ref,\n+\t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width)\n {\n \tstruct commit *current = NULL, *updated;\n@@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn r;\n \t}\n \n@@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = \"\";\n-\t\t\t}\n-\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n \t\t\t\tkind = \"branch\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n \t\t\t\tkind = \"tag\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n \t\t\t\tkind = \"remote-tracking branch\";\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = rm->name;\n \t\t\t}\n@@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t\t  display_state->url_len);\n \n \t\t\tif (ref) {\n-\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n+\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n \t\t\t\t\t\t       rm, summary_width);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n@@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t */\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n-\t\t\t\t\t\t   *what ? what : \"HEAD\",\n+\t\t\t\t\t\t   rm->name,\n \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n \t\t\t}\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 0e45c27007..b9dcdade63 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -54,6 +54,31 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch output with HEAD and --dry-run' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\n+\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * branch            HEAD       -> FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch origin HEAD >actual 2>&1 &&\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         HEAD       -> foo\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch origin HEAD:foo >actual 2>&1 &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476198","messageId":"b545bf8bb944b3f3f74300f77de6101ec6ab393a.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 4/8] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:20Z","receivedAt":"2023-04-27T11:13:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We currently have two different display formats in git-fetch(1) with the\n\"full\" and \"compact\" formats. This is tracked with a boolean value that\nsimply denotes whether the display format is supposed to be compacted\nor not. This works reasonably well while there are only two formats, but\nwe're about to introduce another format that will make this a bit more\nawkward to use.\n\nIntroduce a `enum display_format` that is more readily extensible.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 101 ++++++++++++++++++++++++++++++------------------\n 1 file changed, 64 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7c64f0c562..e03fcd1b2f 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -48,11 +48,17 @@ enum {\n \tTAGS_SET = 2\n };\n \n+enum display_format {\n+\tDISPLAY_FORMAT_UNKNOWN = 0,\n+\tDISPLAY_FORMAT_FULL,\n+\tDISPLAY_FORMAT_COMPACT,\n+};\n+\n struct display_state {\n \tstruct strbuf buf;\n \n \tint refcol_width;\n-\tint compact_format;\n+\tenum display_format format;\n \n \tchar *url;\n \tint url_len, shown_url;\n@@ -784,7 +790,6 @@ static int refcol_width(const struct ref *ref, int compact_format)\n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n \t\t\t       const char *raw_url)\n {\n-\tstruct ref *rm;\n \tconst char *format = \"full\";\n \tint i;\n \n@@ -809,31 +814,42 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \tgit_config_get_string_tmp(\"fetch.output\", &format);\n \tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->compact_format = 0;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n \telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->compact_format = 1;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = 10;\n-\tfor (rm = ref_map; rm; rm = rm->next) {\n-\t\tint width;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tstruct ref *rm;\n \n-\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n-\t\t    !rm->peer_ref ||\n-\t\t    !strcmp(rm->name, \"HEAD\"))\n-\t\t\tcontinue;\n+\t\tdisplay_state->refcol_width = 10;\n+\t\tfor (rm = ref_map; rm; rm = rm->next) {\n+\t\t\tint width;\n \n-\t\twidth = refcol_width(rm, display_state->compact_format);\n+\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n+\t\t\t    !rm->peer_ref ||\n+\t\t\t    !strcmp(rm->name, \"HEAD\"))\n+\t\t\t\tcontinue;\n \n-\t\t/*\n-\t\t * Not precise calculation for compact mode because '*' can\n-\t\t * appear on the left hand side of '->' and shrink the column\n-\t\t * back.\n-\t\t */\n-\t\tif (display_state->refcol_width < width)\n-\t\t\tdisplay_state->refcol_width = width;\n+\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n+\n+\t\t\t/*\n+\t\t\t * Not precise calculation for compact mode because '*' can\n+\t\t\t * appear on the left hand side of '->' and shrink the column\n+\t\t\t * back.\n+\t\t\t */\n+\t\t\tif (display_state->refcol_width < width)\n+\t\t\t\tdisplay_state->refcol_width = width;\n+\t\t}\n+\n+\t\tbreak;\n+\t}\n+\tdefault:\n+\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n \t}\n }\n \n@@ -904,30 +920,41 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *remote, const char *local,\n \t\t\t       int summary_width)\n {\n-\tint width;\n-\n \tif (verbosity < 0)\n \t\treturn;\n \n \tstrbuf_reset(&display_state->buf);\n \n-\tif (!display_state->shown_url) {\n-\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n-\t\t\t    display_state->url_len, display_state->url);\n-\t\tdisplay_state->shown_url = 1;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tint width;\n+\n+\t\tif (!display_state->shown_url) {\n+\t\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n+\t\t\t\t    display_state->url_len, display_state->url);\n+\t\t\tdisplay_state->shown_url = 1;\n+\t\t}\n+\n+\t\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\t\tremote = prettify_refname(remote);\n+\t\tlocal = prettify_refname(local);\n+\n+\t\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n+\n+\t\tif (display_state->format != DISPLAY_FORMAT_COMPACT)\n+\t\t\tprint_remote_to_local(display_state, remote, local);\n+\t\telse\n+\t\t\tprint_compact(display_state, remote, local);\n+\n+\t\tif (error)\n+\t\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\n+\t\tbreak;\n \t}\n-\n-\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n-\tremote = prettify_refname(remote);\n-\tlocal = prettify_refname(local);\n-\n-\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n-\tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, local);\n-\telse\n-\t\tprint_compact(display_state, remote, local);\n-\tif (error)\n-\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n \tfputs(display_state->buf.buf, stderr);\n-- \n2.40.1\n\n"},{"id":"476199","messageId":"4990d35998ea5b6a16acce106edfaf3c68e4a01e.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 5/8] fetch: move display format parsing into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:25Z","receivedAt":"2023-04-27T11:13:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Parsing the display format happens inside of `display_state_init()`. As\nwe only need to check for a simple config entry, this is a natural\nlocation to put this code as it means that display-state logic is neatly\ncontained in a single location.\n\nWe're about to introduce a output format though that is intended to be\nparseable by machines, for example inside of a script. In that case it\nbecomes a bit awkward of an interface if you have to call git-fetch(1)\nwith the `fetch.output` config key set. We're thus going to introduce a\nnew `--output-format` switch for git-fetch(1) so that the output format\ncan be configured more directly.\n\nThis means we'll have to hook parsing of the display format into the\ncommand line options parser. Having the code to determine the actual\noutput format scattered across two different sites is hard to reason\nabout though.\n\nRefactor the code such that callers are expected to pass the display\nformat that is to be used into `display_state_init()`. This allows us to\nlift up the code into the main function, where we can then hook it into\ncommand line options parser in a follow-up commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 41 ++++++++++++++++++++++++-----------------\n 1 file changed, 24 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e03fcd1b2f..bcc156a9ce 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -788,14 +788,13 @@ static int refcol_width(const struct ref *ref, int compact_format)\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n-\t\t\t       const char *raw_url)\n+\t\t\t       const char *raw_url, enum display_format format)\n {\n-\tconst char *format = \"full\";\n \tint i;\n \n \tmemset(display_state, 0, sizeof(*display_state));\n-\n \tstrbuf_init(&display_state->buf, 0);\n+\tdisplay_state->format = format;\n \n \tif (raw_url)\n \t\tdisplay_state->url = transport_anonymize_url(raw_url);\n@@ -812,15 +811,6 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \tif (verbosity < 0)\n \t\treturn;\n \n-\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n-\telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n-\telse\n-\t\tdie(_(\"invalid value for '%s': '%s'\"),\n-\t\t    \"fetch.output\", format);\n-\n \tswitch (display_state->format) {\n \tcase DISPLAY_FORMAT_FULL:\n \tcase DISPLAY_FORMAT_COMPACT: {\n@@ -1614,7 +1604,8 @@ static int backfill_tags(struct display_state *display_state,\n }\n \n static int do_fetch(struct transport *transport,\n-\t\t    struct refspec *rs)\n+\t\t    struct refspec *rs,\n+\t\t    enum display_format display_format)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1700,7 +1691,7 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n-\tdisplay_state_init(&display_state, ref_map, transport->url);\n+\tdisplay_state_init(&display_state, ref_map, transport->url, display_format);\n \n \tif (atomic_fetch) {\n \t\ttransaction = ref_transaction_begin(&err);\n@@ -2076,7 +2067,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n-\t\t     int prune_tags_ok, int use_stdin_refspecs)\n+\t\t     int prune_tags_ok, int use_stdin_refspecs,\n+\t\t     enum display_format display_format)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2143,7 +2135,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs);\n+\texit_code = do_fetch(gtransport, &rs, display_format);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2155,6 +2147,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tconst char *bundle_uri;\n+\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n \tint result = 0;\n@@ -2181,6 +2174,19 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n \n+\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n+\t\tconst char *format = \"full\";\n+\n+\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n+\t\tif (!strcasecmp(format, \"full\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n+\t\telse if (!strcasecmp(format, \"compact\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t    \"fetch.output\", format);\n+\t}\n+\n \tif (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)\n \t\trecurse_submodules = recurse_submodules_cli;\n \n@@ -2309,7 +2315,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t} else if (remote) {\n \t\tif (filter_options.choice || has_promisor_remote())\n \t\t\tfetch_one_setup_partial(remote);\n-\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n+\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n+\t\t\t\t   display_format);\n \t} else {\n \t\tint max_children = max_jobs;\n \n-- \n2.40.1\n\n"},{"id":"476200","messageId":"cfe84129abe5f93282e1305ec6139fb105481b2e.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 6/8] fetch: move option related variables into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:29Z","receivedAt":"2023-04-27T11:13:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The options of git-fetch(1) which we pass to `parse_options()` are\ndeclared globally in `builtin/fetch.c`. This means we're forced to use\nglobal variables for all the options, which is more likely to cause\nconfusion than explicitly passing state around.\n\nRefactor the code to move the options into `cmd_fetch()`. Move variables\nthat were previously forced to be declared globally and which are only\nused by `cmd_fetch()` into function-local scope.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 197 ++++++++++++++++++++++++------------------------\n 1 file changed, 100 insertions(+), 97 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex bcc156a9ce..97a510649c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -75,13 +75,12 @@ static int fetch_prune_tags_config = -1; /* unspecified */\n static int prune_tags = -1; /* unspecified */\n #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */\n \n-static int all, append, dry_run, force, keep, multiple, update_head_ok;\n+static int append, dry_run, force, keep, update_head_ok;\n static int write_fetch_head = 1;\n static int verbosity, deepen_relative, set_upstream, refetch;\n static int progress = -1;\n-static int enable_auto_gc = 1;\n-static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;\n-static int max_jobs = -1, submodule_fetch_jobs_config = -1;\n+static int tags = TAGS_DEFAULT, update_shallow, deepen;\n+static int submodule_fetch_jobs_config = -1;\n static int fetch_parallel_config = 1;\n static int atomic_fetch;\n static enum transport_family family;\n@@ -92,17 +91,11 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n-static const char *submodule_prefix = \"\";\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n-static int fetch_write_commit_graph = -1;\n-static int stdin_refspecs = 0;\n-static int negotiate_only;\n \n static int git_fetch_config(const char *k, const char *v, void *cb)\n {\n@@ -160,92 +153,6 @@ static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-static struct option builtin_fetch_options[] = {\n-\tOPT__VERBOSITY(&verbosity),\n-\tOPT_BOOL(0, \"all\", &all,\n-\t\t N_(\"fetch from all remotes\")),\n-\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n-\t\t N_(\"set upstream for git pull/fetch\")),\n-\tOPT_BOOL('a', \"append\", &append,\n-\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n-\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n-\t\t N_(\"use atomic transaction to update references\")),\n-\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to upload pack on remote end\")),\n-\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n-\tOPT_BOOL('m', \"multiple\", &multiple,\n-\t\t N_(\"fetch from multiple remotes\")),\n-\tOPT_SET_INT('t', \"tags\", &tags,\n-\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n-\tOPT_SET_INT('n', NULL, &tags,\n-\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules fetched in parallel\")),\n-\tOPT_BOOL(0, \"prefetch\", &prefetch,\n-\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n-\tOPT_BOOL('p', \"prune\", &prune,\n-\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n-\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n-\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n-\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n-\t\t    N_(\"control recursive fetching of submodules\"),\n-\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"dry-run\", &dry_run,\n-\t\t N_(\"dry run\")),\n-\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n-\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n-\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n-\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n-\t\t    N_(\"allow updating of HEAD ref\")),\n-\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n-\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n-\t\t   N_(\"deepen history of shallow clone\")),\n-\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n-\t\t   N_(\"deepen history of shallow repository based on time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n-\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n-\t\t    N_(\"deepen history of shallow clone\")),\n-\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n-\t\t      N_(\"convert to a complete repository\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n-\t\t      N_(\"re-fetch without negotiating common commits\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n-\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n-\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n-\t\t   N_(\"default for recursive fetching of submodules \"\n-\t\t      \"(lower priority than config files)\"),\n-\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n-\t\t N_(\"accept refs that update .git/shallow\")),\n-\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n-\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n-\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV4),\n-\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV6),\n-\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n-\t\t\tN_(\"report that we have only objects reachable from this object\")),\n-\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n-\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n-\t\t N_(\"check for forced-updates on all updated branches\")),\n-\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n-\t\t N_(\"write the commit-graph after fetching\")),\n-\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n-\t\t N_(\"accept refspecs from stdin\")),\n-\tOPT_END()\n-};\n-\n static void unlock_pack(unsigned int flags)\n {\n \tif (gtransport)\n@@ -2145,13 +2052,109 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n-\tint i;\n \tconst char *bundle_uri;\n+\tconst char *submodule_prefix = \"\";\n \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n+\tint all = 0, multiple = 0;\n \tint result = 0;\n \tint prune_tags_ok = 1;\n+\tint enable_auto_gc = 1;\n+\tint unshallow = 0;\n+\tint max_jobs = -1;\n+\tint recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n+\tint recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n+\tint fetch_write_commit_graph = -1;\n+\tint stdin_refspecs = 0;\n+\tint negotiate_only = 0;\n+\tint i;\n+\n+\tstruct option builtin_fetch_options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_BOOL(0, \"all\", &all,\n+\t\t\t N_(\"fetch from all remotes\")),\n+\t\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n+\t\t\t N_(\"set upstream for git pull/fetch\")),\n+\t\tOPT_BOOL('a', \"append\", &append,\n+\t\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n+\t\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n+\t\t\t N_(\"use atomic transaction to update references\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to upload pack on remote end\")),\n+\t\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n+\t\tOPT_BOOL('m', \"multiple\", &multiple,\n+\t\t\t N_(\"fetch from multiple remotes\")),\n+\t\tOPT_SET_INT('t', \"tags\", &tags,\n+\t\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n+\t\tOPT_SET_INT('n', NULL, &tags,\n+\t\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules fetched in parallel\")),\n+\t\tOPT_BOOL(0, \"prefetch\", &prefetch,\n+\t\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n+\t\tOPT_BOOL('p', \"prune\", &prune,\n+\t\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n+\t\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n+\t\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n+\t\t\t    N_(\"control recursive fetching of submodules\"),\n+\t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n+\t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n+\t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n+\t\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n+\t\t\t    N_(\"allow updating of HEAD ref\")),\n+\t\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n+\t\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n+\t\t\t   N_(\"deepen history of shallow clone\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n+\t\t\t   N_(\"deepen history of shallow repository based on time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n+\t\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n+\t\t\t    N_(\"deepen history of shallow clone\")),\n+\t\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n+\t\t\t      N_(\"convert to a complete repository\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n+\t\t\t      N_(\"re-fetch without negotiating common commits\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n+\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n+\t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n+\t\t\t   N_(\"default for recursive fetching of submodules \"\n+\t\t\t      \"(lower priority than config files)\"),\n+\t\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n+\t\t\t N_(\"accept refs that update .git/shallow\")),\n+\t\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n+\t\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV4),\n+\t\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV6),\n+\t\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n+\t\t\t\tN_(\"report that we have only objects reachable from this object\")),\n+\t\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n+\t\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n+\t\t\t N_(\"check for forced-updates on all updated branches\")),\n+\t\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n+\t\t\t N_(\"write the commit-graph after fetching\")),\n+\t\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n+\t\t\t N_(\"accept refspecs from stdin\")),\n+\t\tOPT_END()\n+\t};\n \n \tpacket_trace_identity(\"fetch\");\n \n-- \n2.40.1\n\n"},{"id":"476201","messageId":"0335e5eeb4ded336c5ff7c8888c8aab9dfed2505.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:33Z","receivedAt":"2023-04-27T11:13:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is only possible to configure the output format that git-fetch(1)\nuses by setting it via a config key. While this interface may be fine as\nlong as we only have the current \"full\" and \"compact\" output formats,\nwhere it is unlikely that the user will have to change them regularly.\nBut we're about to introduce a new machine-parseable interface where the\ncurrent mechanism feels a little bit indirect and rigid.\n\nIntroduce a new `--output-format` option that allows the user to change\nthe desired format more directly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/fetch-options.txt |  5 +++\n builtin/fetch.c                 | 48 ++++++++++++++++++----\n t/t5574-fetch-output.sh         | 72 +++++++++++++++++++++++++++------\n 3 files changed, 106 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 622bd84768..654f96f79d 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -78,6 +78,11 @@ linkgit:git-config[1].\n --dry-run::\n \tShow what would be done, without making any changes.\n \n+--output-format::\n+\tControl how ref update status is printed. Valid values are\n+\t`full` and `compact`. Default value is `full`. See section\n+\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\n ifndef::git-pull[]\n --[no-]write-fetch-head::\n \tWrite the list of remote refs fetched in the `FETCH_HEAD`\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 97a510649c..30099b2ac3 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,13 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_MAX,\n+};\n+\n+static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n+\tNULL,\n+\t\"full\",\n+\t\"compact\",\n };\n \n struct display_state {\n@@ -1879,7 +1886,8 @@ static int fetch_finished(int result, struct strbuf *out,\n \treturn 0;\n }\n \n-static int fetch_multiple(struct string_list *list, int max_children)\n+static int fetch_multiple(struct string_list *list, int max_children,\n+\t\t\t  enum display_format format)\n {\n \tint i, result = 0;\n \tstruct strvec argv = STRVEC_INIT;\n@@ -1894,6 +1902,9 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\t     \"--no-write-commit-graph\", NULL);\n \tadd_options_to_argv(&argv);\n \n+\tif (format != DISPLAY_FORMAT_UNKNOWN)\n+\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n+\n \tif (max_children != 1 && list->nr != 1) {\n \t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n \t\tconst struct run_process_parallel_opts opts = {\n@@ -2050,6 +2061,29 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \treturn exit_code;\n }\n \n+static enum display_format parse_display_format(const char *format)\n+{\n+\tfor (int i = 0; i < ARRAY_SIZE(display_formats); i++)\n+\t\tif (display_formats[i] && !strcmp(display_formats[i], format))\n+\t\t\treturn i;\n+\treturn DISPLAY_FORMAT_UNKNOWN;\n+}\n+\n+static int opt_parse_output_format(const struct option *opt, const char *arg, int unset)\n+{\n+\tenum display_format *format = opt->value, parsed;\n+\n+\tif (unset || !arg)\n+\t\treturn 1;\n+\n+\tparsed = parse_display_format(arg);\n+\tif (parsed == DISPLAY_FORMAT_UNKNOWN)\n+\t\treturn error(_(\"unsupported output format '%s'\"), arg);\n+\t*format = parsed;\n+\n+\treturn 0;\n+}\n+\n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tconst char *bundle_uri;\n@@ -2102,6 +2136,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n \t\t\t N_(\"dry run\")),\n+\t\tOPT_CALLBACK(0, \"output-format\", &display_format, N_(\"format\"), N_(\"output format\"),\n+\t\t\t     opt_parse_output_format),\n \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n@@ -2181,11 +2217,9 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tconst char *format = \"full\";\n \n \t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\t\tif (!strcasecmp(format, \"full\"))\n-\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n-\t\telse if (!strcasecmp(format, \"compact\"))\n-\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n-\t\telse\n+\n+\t\tdisplay_format = parse_display_format(format);\n+\t\tif (display_format == DISPLAY_FORMAT_UNKNOWN)\n \t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t\t    \"fetch.output\", format);\n \t}\n@@ -2339,7 +2373,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\tmax_children = fetch_parallel_config;\n \n \t\t/* TODO should this also die if we have a previous partial-clone? */\n-\t\tresult = fetch_multiple(&list, max_children);\n+\t\tresult = fetch_multiple(&list, max_children, display_format);\n \t}\n \n \ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex b9dcdade63..662c960f94 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -24,14 +24,37 @@ test_expect_success 'fetch with invalid output format configuration' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch with invalid output format via command line' '\n+\ttest_must_fail git fetch --output-format >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: option \\`output-format${SQ} requires a value\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git fetch --output-format= origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: unsupported output format ${SQ}${SQ}\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\ttest_must_fail git fetch --output-format=garbage origin >actual 2>&1 &&\n+\tcat >expect <<-EOF &&\n+\terror: unsupported output format ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n+\ttest_when_finished \"rm -rf full-cfg full-cli\" &&\n+\tgit clone . full-cfg &&\n+\tgit clone . full-cli &&\n \ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n+\n+\tgit -C full-cfg -c fetch.output=full fetch origin >actual-cfg 2>&1 &&\n+\tgit -C full-cli fetch --output-format=full origin >actual-cli 2>&1 &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\n+\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n \tcat >expect <<-\\EOF &&\n \tmain                 -> origin/main\n \tlooooooooooooong-tag -> looooooooooooong-tag\n@@ -40,13 +63,16 @@ test_expect_success 'fetch aligned output' '\n '\n \n test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n+\ttest_when_finished \"rm -rf compact-cfg compact-cli\" &&\n+\tgit clone . compact-cli &&\n+\tgit clone . compact-cfg &&\n \ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n+\n+\tgit -C compact-cfg -c fetch.output=compact fetch origin >actual-cfg 2>&1 &&\n+\tgit -C compact-cli fetch --output-format=compact origin >actual-cli 2>&1 &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\n+\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n \tcat >expect <<-\\EOF &&\n \tmain       -> origin/*\n \textraaa    -> *\n@@ -54,6 +80,28 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch compact output with multiple remotes' '\n+\ttest_when_finished \"rm -rf compact-cfg compact-cli\" &&\n+\n+\tgit clone . compact-cli &&\n+\tgit -C compact-cli remote add second-remote \"$PWD\" &&\n+\tgit clone . compact-cfg &&\n+\tgit -C compact-cfg remote add second-remote \"$PWD\" &&\n+\ttest_commit multi-commit &&\n+\n+\tgit -C compact-cfg -c fetch.output=compact fetch --all >actual-cfg 2>&1 &&\n+\tgit -C compact-cli fetch --output-format=compact --all >actual-cli 2>&1 &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\n+\tgrep -e \"->\" actual-cfg | cut -c 22- >actual &&\n+\tcat >expect <<-\\EOF &&\n+\tmain         -> origin/*\n+\tmulti-commit -> *\n+\tmain       -> second-remote/*\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch output with HEAD and --dry-run' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n-- \n2.40.1\n\n"},{"id":"476202","messageId":"d7c1bc1a80406ad320c2de684e0c97ba14827c7a.1682593865.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1682593865.git.ps@pks.im","subject":"[PATCH v2 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-27T11:13:38Z","receivedAt":"2023-04-27T11:14:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The output of git-fetch(1) is obviously designed for consumption by\nusers, only: we neatly columnize data, we abbreviate reference names, we\nprint neat arrows and we don't provide information about actual object\nIDs that have changed. This makes the output format basically unusable\nin the context of scripted invocations of git-fetch(1) that want to\nlearn about the exact changes that the command performs.\n\nIntroduce a new machine-parseable \"porcelain\" output format that is\nsupposed to fix this shortcoming. This output format is intended to\nprovide information about every reference that is about to be updated,\nthe old object ID that the reference has been pointing to and the new\nobject ID it will be updated to. Furthermore, the output format provides\nthe same flags as the human-readable format to indicate basic conditions\nfor each reference update like whether it was a fast-forward update, a\nbranch deletion, a rejected update or others.\n\nThe output format is quite simple:\n\n```\n<flag> <old-object-id> <new-object-id> <local-reference>\\n\n```\n\nWe assume two conditions which are generally true:\n\n    - The old and new object IDs have fixed known widths and cannot\n      contain spaces.\n\n    - References cannot contain newlines.\n\nWith these assumptions, the output format becomes unambiguously\nparseable. Furthermore, given that this output is designed to be\nconsumed by scripts, the machine-readable data is printed to stdout\ninstead of stderr like the human-readable output is. This is mostly done\nso that other data printed to stderr, like error messages or progress\nmeters, don't interfere with the parseable data.\n\nA notable ommission here is that the output format does not include the\nremote from which a reference was fetched, which might be important\ninformation especially in the context of multi-remote fetches. But as\nsuch a format would require us to print the remote for every single\nreference update due to parallelizable fetches it feels wasteful for the\nmost likely usecase, which is when fetching from a single remote. If\nusecases come up for this in the future though it is easy enough to add\na new \"porcelain-v2\" format that adds this information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/fetch.txt  |  4 +-\n Documentation/fetch-options.txt |  4 +-\n Documentation/git-fetch.txt     | 17 ++++++-\n builtin/fetch.c                 | 47 +++++++++++++-----\n t/t5574-fetch-output.sh         | 84 +++++++++++++++++++++++++++++++++\n 5 files changed, 139 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/fetch.txt b/Documentation/config/fetch.txt\nindex 568f0f75b3..70734226c0 100644\n--- a/Documentation/config/fetch.txt\n+++ b/Documentation/config/fetch.txt\n@@ -52,8 +52,8 @@ fetch.pruneTags::\n \n fetch.output::\n \tControl how ref update status is printed. Valid values are\n-\t`full` and `compact`. Default value is `full`. See section\n-\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\t`full`, `compact` and `porcelain`. Default value is `full`.\n+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n \n fetch.negotiationAlgorithm::\n \tControl how information about the commits in the local repository\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 654f96f79d..5ca8a67fe8 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -80,8 +80,8 @@ linkgit:git-config[1].\n \n --output-format::\n \tControl how ref update status is printed. Valid values are\n-\t`full` and `compact`. Default value is `full`. See section\n-\tOUTPUT in linkgit:git-fetch[1] for detail.\n+\t`full`, `compact` and `porcelain`. Default value is `full`.\n+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n \n ifndef::git-pull[]\n --[no-]write-fetch-head::\ndiff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\nindex fba66f1460..efd22cd372 100644\n--- a/Documentation/git-fetch.txt\n+++ b/Documentation/git-fetch.txt\n@@ -197,13 +197,26 @@ The output of \"git fetch\" depends on the transport method used; this\n section describes the output when fetching over the Git protocol\n (either locally or via ssh) and Smart HTTP protocol.\n \n-The status of the fetch is output in tabular form, with each line\n-representing the status of a single ref. Each line is of the form:\n+The output format can be chosen either via the `fetch.output` config\n+(see linkgit:git-config[1]), or via the `--output-format` switch.\n+Supported values include:\n+\n+For the `full` and `compact` output formats, the status of the fetch is\n+output in tabular, with each line representing the status of a single\n+ref. Each line is of the form:\n \n -------------------------------\n  <flag> <summary> <from> -> <to> [<reason>]\n -------------------------------\n \n+The `porcelain` output format is intended to be machine-parseable. In\n+contrast to the human-readable output formats it thus prints to standard\n+output instead of standard error. Each line is of the form:\n+\n+-------------------------------\n+<flag> <old-object-id> <new-object-id> <local-reference>\n+-------------------------------\n+\n The status of up-to-date refs is shown only if the --verbose option is\n used.\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 30099b2ac3..abe6b8879d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,7 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_PORCELAIN,\n \tDISPLAY_FORMAT_MAX,\n };\n \n@@ -59,6 +60,7 @@ static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n \tNULL,\n \t\"full\",\n \t\"compact\",\n+\t\"porcelain\",\n };\n \n struct display_state {\n@@ -752,8 +754,11 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\t/* We don't need to precompute anything here. */\n+\t\tbreak;\n \tdefault:\n-\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n }\n \n@@ -822,8 +827,12 @@ static void print_compact(struct display_state *display_state,\n static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *summary, const char *error,\n \t\t\t       const char *remote, const char *local,\n+\t\t\t       const struct object_id *old_oid,\n+\t\t\t       const struct object_id *new_oid,\n \t\t\t       int summary_width)\n {\n+\tFILE *f = stderr;\n+\n \tif (verbosity < 0)\n \t\treturn;\n \n@@ -856,12 +865,17 @@ static void display_ref_update(struct display_state *display_state, char code,\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\tstrbuf_addf(&display_state->buf, \"%c %s %s %s\", code,\n+\t\t\t    oid_to_hex(old_oid), oid_to_hex(new_oid), local);\n+\t\tf = stdout;\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n-\tfputs(display_state->buf.buf, stderr);\n+\tfputs(display_state->buf.buf, f);\n }\n \n static int update_local_ref(struct ref *ref,\n@@ -879,7 +893,8 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -892,7 +907,8 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -903,12 +919,14 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -941,7 +959,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn r;\n \t}\n \n@@ -963,7 +982,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -975,12 +995,14 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1221,7 +1243,9 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n \t\t\t\t\t\t   rm->name,\n-\t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n+\t\t\t\t\t\t   \"FETCH_HEAD\",\n+\t\t\t\t\t\t   &rm->new_oid, &rm->old_oid,\n+\t\t\t\t\t\t   summary_width);\n \t\t\t}\n \t\t}\n \t}\n@@ -1361,6 +1385,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\tdisplay_ref_update(display_state, '-', _(\"[deleted]\"), NULL,\n \t\t\t\t\t   _(\"(none)\"), ref->name,\n+\t\t\t\t\t   &ref->new_oid, &ref->old_oid,\n \t\t\t\t\t   summary_width);\n \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 662c960f94..d88ad8af31 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -102,6 +102,72 @@ test_expect_success 'fetch compact output with multiple remotes' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output' '\n+\ttest_when_finished \"rm -rf porcelain-cfg porcelain-cli\" &&\n+\n+\t# Set up a bunch of references that we can use to demonstrate different\n+\t# kinds of flag symbols in the output format.\n+\tMAIN_OLD=$(git rev-parse HEAD) &&\n+\tgit branch \"fast-forward\" &&\n+\tgit branch \"deleted-branch\" &&\n+\tgit checkout -b force-updated &&\n+\ttest_commit --no-tag force-update-old &&\n+\tFORCE_UPDATED_OLD=$(git rev-parse HEAD) &&\n+\tgit checkout main &&\n+\n+\t# Clone and pre-seed the repositories. We fetch references into two\n+\t# namespaces so that we can test that rejected and force-updated\n+\t# references are reported properly.\n+\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n+\tgit clone . porcelain-cli &&\n+\tgit clone . porcelain-cfg &&\n+\tgit -C porcelain-cfg fetch origin $refspecs &&\n+\tgit -C porcelain-cli fetch origin $refspecs &&\n+\n+\t# Now that we have set up the client repositories we can change our\n+\t# local references.\n+\tgit branch new-branch &&\n+\tgit branch -d deleted-branch &&\n+\tgit checkout fast-forward &&\n+\ttest_commit --no-tag fast-forward-new &&\n+\tFAST_FORWARD_NEW=$(git rev-parse HEAD) &&\n+\tgit checkout force-updated &&\n+\tgit reset --hard HEAD~ &&\n+\ttest_commit --no-tag force-update-new &&\n+\tFORCE_UPDATED_NEW=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n+\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n+\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\tEOF\n+\n+\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n+\t# and non-dry-run fetches produces the same output. Execution of the\n+\t# fetch is expected to fail as we have a rejected reference update.\n+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --dry-run --prune origin $refspecs >actual-dry-run-cfg &&\n+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --dry-run --prune origin $refspecs >actual-dry-run-cli &&\n+\ttest_cmp actual-dry-run-cfg actual-dry-run-cli &&\n+\ttest_cmp expect actual-dry-run-cfg &&\n+\n+\t# And now we perform a non-dry-run fetch.\n+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --prune origin $refspecs >actual-cfg &&\n+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --prune origin $refspecs >actual-cli &&\n+\ttest_cmp actual-cfg actual-cli &&\n+\ttest_cmp expect actual-cfg &&\n+\n+\t# Ensure that the dry-run and non-dry-run output matches.\n+\ttest_cmp actual-dry-run-cfg actual-cfg\n+'\n+\n test_expect_success 'fetch output with HEAD and --dry-run' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n@@ -127,6 +193,24 @@ test_expect_success 'fetch output with HEAD and --dry-run' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\tCOMMIT_ID=$(git rev-parse HEAD) &&\n+\n+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476217","messageId":"kl6lo7n9me29.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"d1fb6eeae76bc550030475e40868b90a2ca6ce8c.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-27T17:26:38Z","receivedAt":"2023-04-27T17:26:53Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"For v2, I've only read through this patch for now. I'm still formulating\nmy thoughts on the output format and the CLI option name.\n\nPatrick Steinhardt <ps@pks.im> writes:\n\n> `store_updated_refs()` parses the remote reference for two purposes:\n>\n>     - It gets used as a note when writing FETCH_HEAD.\n>\n>     - It is passed through to `display_ref_update()` to display\n>       updated references in the following format:\n>\n>       ```\n>        * branch               master          -> master\n>       ```\n>\n> In most cases, the parsed remote reference is the prettified reference\n> name and can thus be used for both cases. But if the remote reference is\n> HEAD, the parsed remote reference becomes empty. This is intended when\n> we write the FETCH_HEAD, where we skip writing the note in that case.\n> But it is not intended when displaying the updated references and would\n> cause us to miss the left-hand side of the displayed reference update:\n>\n> [...]\n>\n> As the return value of `prettify_refname()` would do the correct thing\n> for us in all three cases, we can fix the bug by passing through the\n> full remote reference name to `display_ref_update()`, which learns to\n> call `prettify_refname()`. At the same time, this also simplifies the\n> code a bit.\n\nThis rewritten section explains the bug and the fix very nicely :)\nThanks!\n\n> diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> index 0e45c27007..b9dcdade63 100755\n> --- a/t/t5574-fetch-output.sh\n> +++ b/t/t5574-fetch-output.sh\n> @@ -54,6 +54,31 @@ test_expect_success 'fetch compact output' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'fetch output with HEAD and --dry-run' '\n> +\ttest_when_finished \"rm -rf head\" &&\n> +\tgit clone . head &&\n> +\n> +\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n> +\tcat >expect <<-EOF &&\n> +\tFrom $(test-tool path-utils real_path .)/.\n> +\t * branch            HEAD       -> FETCH_HEAD\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\tgit -C head fetch origin HEAD >actual 2>&1 &&\n> +\ttest_cmp expect actual &&\n\nMaybe rename the test to just 'fetch output with HEAD'? The 'and\n--dry-run' part seems completely obsolete.\n"},{"id":"476228","messageId":"82a6ad96-ade5-2cfe-cd6b-0b6b94b6658f@intel.com","threadId":"59619","inReplyTo":"ZEpVatqnhdUSMWYY@ncase","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-27T19:46:10Z","receivedAt":"2023-04-27T19:46:22Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/27/2023 3:58 AM, Patrick Steinhardt wrote:\n> On Wed, Apr 26, 2023 at 01:23:12PM -0700, Junio C Hamano wrote:\n> Furthermore, I think that `--[output-]format` has the advantage that you\n> don't need to handle priorities or mutual exclusion of different options\n> that all apply to the reference format. To a user, it is not immediately\n> obvious what `git fetch --format=compact --porcelain` would do, and\n> which of both options ultimately get respected. But that's likely only\n> true for future commands, because any migration would create the same\n> kind of ambiguity for preexisting commands.\n> \n> If we were to also migrate preexisting code to use `--[output-]format`\n> then I'd argue that `--output-format` is likely the better name, mostly\n> because it is less likely to be ambiguous compared to `--format`. The\n> latter could e.g. easily confused with `--object-format`.\n> \n> So I think I'll stick with `--output-format` for the time being.\n> \n\nI agree. I think using --output-format and migrating existing commands\nthat have --porcelain to use --output-format is good. I'm not sure\nwhether to keep using --output-format=porcelain or whether to use a\ndifferent term there.\n\nI definitely think avoiding confusion with --format is good, and I don't\nthink its too much of a burden to have the longer --output-format as the\noption name.\n\nThanks,\nJake\n\n> Patrick\n"},{"id":"476229","messageId":"09fe302a-6d80-4a95-85f8-a9a9f4d655ee@intel.com","threadId":"59619","inReplyTo":"d1fb6eeae76bc550030475e40868b90a2ca6ce8c.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 3/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-27T19:49:26Z","receivedAt":"2023-04-27T19:49:35Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/27/2023 4:13 AM, Patrick Steinhardt wrote:\n> index 0e45c27007..b9dcdade63 100755\n> --- a/t/t5574-fetch-output.sh\n> +++ b/t/t5574-fetch-output.sh\n> @@ -54,6 +54,31 @@ test_expect_success 'fetch compact output' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'fetch output with HEAD and --dry-run' '\n> +\ttest_when_finished \"rm -rf head\" &&\n\nThe test name could drop the --dry-run mention since this now tests both\nvariants.\n\n\nEverything else looks good on this patch. The new commit message is much\nbetter to me now!\n"},{"id":"476230","messageId":"2876e9b5-b9a6-713d-8c0c-0561e1c6d6da@intel.com","threadId":"59619","inReplyTo":"d7c1bc1a80406ad320c2de684e0c97ba14827c7a.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2023-04-27T19:52:36Z","receivedAt":"2023-04-27T19:52:47Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 4/27/2023 4:13 AM, Patrick Steinhardt wrote:\n> A notable ommission here is that the output format does not include the\n> remote from which a reference was fetched, which might be important\n> information especially in the context of multi-remote fetches. But as\n> such a format would require us to print the remote for every single\n> reference update due to parallelizable fetches it feels wasteful for the\n> most likely usecase, which is when fetching from a single remote. If\n> usecases come up for this in the future though it is easy enough to add\n> a new \"porcelain-v2\" format that adds this information.\n\nThanks for clarifying this. I think generally its ok to leave out since\nyou can often infer what remote a ref came from (either because of\nrefs/remotes, or because of how you configured your refspecs for fetching).\n\nWe do have the option to extend this in the future if needed, so I think\nthat's ok. In that case if we wanted an even more verbose output I'd\nprobably argue for something more structured like adding it as part of a\nJSON output.\n\nThanks,\nJake\n"},{"id":"476237","messageId":"xmqqedo5hu10.fsf@gitster.g","threadId":"59619","inReplyTo":"cfe84129abe5f93282e1305ec6139fb105481b2e.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 6/8] fetch: move option related variables into main function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-04-27T21:52:59Z","receivedAt":"2023-04-27T21:53:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The options of git-fetch(1) which we pass to `parse_options()` are\n> declared globally in `builtin/fetch.c`. This means we're forced to use\n> global variables for all the options, which is more likely to cause\n> confusion than explicitly passing state around.\n>\n> Refactor the code to move the options into `cmd_fetch()`. Move variables\n> that were previously forced to be declared globally and which are only\n> used by `cmd_fetch()` into function-local scope.\n\nVery well done.\n"},{"id":"476238","messageId":"xmqq8redhtn6.fsf@gitster.g","threadId":"59619","inReplyTo":"0335e5eeb4ded336c5ff7c8888c8aab9dfed2505.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-04-27T22:01:17Z","receivedAt":"2023-04-27T22:01:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 97a510649c..30099b2ac3 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -52,6 +52,13 @@ enum display_format {\n>  \tDISPLAY_FORMAT_UNKNOWN = 0,\n>  \tDISPLAY_FORMAT_FULL,\n>  \tDISPLAY_FORMAT_COMPACT,\n> +\tDISPLAY_FORMAT_MAX,\n> +};\n> +\n> +static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n> +\tNULL,\n> +\t\"full\",\n> +\t\"compact\",\n>  };\n\nHmph, the _MAX thing that is only needed to size the array and never\nused elsewhere (i.e. parse_display_format() uses ARRAY_SIZE() of the\nthing, instead of the constant, and that is just fine) is an eyesore.\n\nI wonder if\n\n\tstatic const char *const display_format[] = {\n\t\t[DISPLAY_FORMAT_UNKNOWN] = NULL,\n\t\t[DISPLAY_FORMAT_FULL] = \"full\",\n\t\t[DISPLAY_FORMAT_COMPACT] = \"compact\",\n\t};\n\nwould be easier to maintain?\n\nI'll omit my usual \"name your array in singular\" lecture, as I think\nyou've heard it already.\n\nThanks.\n\n\n"},{"id":"476246","messageId":"kl6lildhlz3i.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"xmqqildis89b.fsf@gitster.g","subject":"Re: [PATCH 0/8] fetch: introduce machine-parseable output","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-27T22:49:53Z","receivedAt":"2023-04-27T22:50:01Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> We had some discussion during review club about this, where the idea of\n>> using \"--porcelain\" came up because many commands use that when\n>> switching into a machine readable format.\n>>\n>> In addition, this format not only changes the output but also moves it\n>> from being on stderr to stdout, which is a hint that the intended usage\n>> of the command is now a little different.\n>\n> A little different from what?  I do not think the answer would be\n> \"other program's --porcelain mode\", as sending them to stdout would\n> be one of the things that make the output easier for programs to\n> parse, so it does sound like very much in the same spirit as \"git\n> status --porcelain\" where its output format gets tweaked to be more\n> machine friendly.\n>\n> The output with \"--porcelain\" option enabled tend to be less human\n> friendly and the distinction between Porcelain (for humans) and\n> plumbing (for scripts) is reversed in the use of the word there---it\n> started as \"this is the option for those who write Porcelain\n> commands to use\", but still it is not a very good name for the\n> option.\n>\n> I am perfectly OK if the plan is to uniformly use --output-format\n> (or something equally more descriptive) and migrate and deprecate\n> the \"--porcelain\" option away from existing commands.\n\nI agree that --porcelain is a confusing name that would be nice to\ndeprecate, but I don't think --output-format captures all of the intent\nof \"operate in a machine-friendly mode instead of a human-friendly one\".\nUnfortunately, if we had picked --plumbing from the beginning, I doubt\nwe would be having this discussion today :/\n\nE.g. machines (Unix ones at least?) like to have output on stdout and to\nbe able to request NUL-terminated output. It's unfortunate that if we\ndon't piggyback onto --output-format, we run into option precedence\nproblems (like Patrick mentioned), but I'd find it more confusing that\n--output-format=[porcelain|full|compact] don't behave the same way.\n\nI don't think this puts us in a better spot with regards to option\nprecedence either. Consider:\n\n  git fetch --output-format=full -z <...>\n\nThe only way to respect both options is to have -z affect the\nhuman-readable output, which isn't the end of the world, but it seems\nunnecessary.\n\nPerhaps something like --machine instead?\n"},{"id":"476250","messageId":"kl6lfs8knc9f.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"ZEpVSrz-uUcfN_3_@ncase","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-27T23:20:12Z","receivedAt":"2023-04-27T23:20:17Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Apr 26, 2023 at 12:52:46PM -0700, Glen Choo wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> \n>> > The output format is quite simple:\n>> >\n>> > ```\n>> > <flag> <old-object-id> <new-object-id> <local-reference>\\n\n>> > ```\n>> \n>> This format doesn't show the remote name or url that was fetched. That\n>> seems okay when fetching with a single remote, but it seems necessary\n>> with \"--all\". Perhaps you were planning to add that in a later series?\n>> If so, I think it's okay to call the \"porcelain\" format experimental,\n>> and forbid porcelain + --all until then.\n>\n> The reason is mostly that I didn't find an output format that I really\n> liked here. We'd basically have to repeat the remote URL for every\n> single reference: just repeating it once per remote doesn't fly because\n> with `--parallel` the output could be intermingled. But doing that feels\n> wasteful to me, so I bailed. I guess I'm also biased here because it\n> just wouldn't be useful to myself.\n\nYeah, I couldn't think of a good way to output the remote URL either.\n\n> So with that in mind, I'd like to continue ignoring this issue for now\n> and just not report the remote that the ref came from. But I'd also\n> argue that we don't have to restrict porcelain mode to single-remote\n> fetches: it can still be useful to do multi-remote fetches even without\n> the information where a certain reference update comes from. So any kind\n> of restriction would feel artificial to me here.\n>\n> Furthermore, I'd argue that it is not necessary to label the format as\n> experimental only because of this limitation. With the refactorings done\n> in this and the preceding patch series it is easy to add a new format in\n> case there indeed is somebody that would have a usecase for this. The\n> \"porcelain\" format should stay stable, and if we decide that we want to\n> also report the remote for each reference in a follow-up we can easily\n> add a \"porcelain-v2\" or \"porcelain-with-remote\" format.\n\nI agree that the current form is useful, but it seems quite likely that\nusers will want the remote name/url at some point. In the majority of\ncases, the user can parse the remote name from the updated local ref and\nif they want, they look up the url by asking \"git config\", but that\nbreaks down quite quickly if they have a custom refspec.\n\nMy suggestion to label it experimental was meant to give us the freedom\nto change the \"first version\" in backwards-incompatible ways that we\notherwise couldn't. We could say \"here's porcelain-v2, use this and\nignore porcelain-v1\", but it would be even nicer if the user didn't need\nto think about porcelain-v1. The idea is that we could call it\nexperimental, see what users need in the wild, then make changes without\nhaving to create a new \"porcelain-v2\" mode.\n\n(I realize in hindsight that calling it experimental sounds a bit\ninsulting. I thought your proposal was quite sensible, actually. i\napologize if it came across that way)\n\nPerhaps part of the reason why you'd prefer not to make it experimental\nis that users who might want this feature are too afraid of\nbackwards-incompatible changes. I think that's fair. A different way of\nachieving the same flexibility would be to make the \"porcelain\" format\nextensible (like git status --porcelain=v2). A future, multivalued\n\"--porcelain-options=show-remote-url,show-remote-ref\" would cover\nour bases.\n\nI'm happy with either approach as long as we don't have to bikeshed\nabout the \"perfect\" porcelain output :)\n\n>> > With these assumptions, the output format becomes unambiguously\n>> > parseable. Furthermore, given that this output is designed to be\n>> > consumed by scripts, the machine-readable data is printed to stdout\n>> > instead of stderr like the human-readable output is. This is mostly done\n>> > so that other data printed to stderr, like error messages or progress\n>> > meters, don't interfere with the parseable data.\n>> \n>> Sending the 'main output' to stdout makes sense to me, but this (and\n>> possibly respecting -z) sounds like a different mode of operation, not\n>> just a matter of formats. It seems different enough that I'd prefer not\n>> to piggyback on \"fetch.output\" for this (even though this adds more\n>> surface to the interface...).\n>> \n>> We could add --porcelain and say that \"fetch.output\" is ignored if\n>> --porcelain is also given. That also eliminates the need for\n>> --output-format, I think.\n>\n> I was thinking about this initially, as well. But ultimately I decided\n> against this especially because of your second paragraph: we'd now need\n> to think about precedence of options and mutual exclusion, and that to\n> me feels like an interface that is less obvious than a single knob that\n> works as you'd expect.\n\nMy full thoughts on this are in\n\n  https://lore.kernel.org/git/kl6lildhlz3i.fsf@chooglen-macbookpro.roam.corp.google.com\n\nbut the short version is that I'm not sure if I expect something as\ninnocuous-looking as --output-format would imply other, machine-friendly\nthings (like stdout instead of stderr), and using --porcelain might make\noption precedence clearer in some situtations (like if -z is given).\n\n>> > +test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n>> > +\ttest_when_finished \"rm -rf head\" &&\n>> > +\tgit clone . head &&\n>> > +\tCOMMIT_ID=$(git rev-parse HEAD) &&\n>> > +\n>> > +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n>> > +\tcat >expect <<-EOF &&\n>> > +\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n>> > +\tEOF\n>> > +\ttest_cmp expect actual &&\n>> > +\n>> > +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n>> > +\tcat >expect <<-EOF &&\n>> > +\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n>> > +\tEOF\n>> > +\ttest_cmp expect actual\n>> > +'\n>> \n>> As mentioned upthread, I think this test isn't needed because\n>> \"porcelain\" wouldn't run into the bug we are checking for anyway.\n>\n> The only reason that the other bug was able to survive for so long was\n> that we didn't have test coverage there. So I think it makes sense to\n> explicitly test this, too, also because it causes us to walk a different\n> code path.\n>\n> Last but not least: this test uncovered a segfault I had in a previous\n> version. So I'd rather keep it :)\n\nAh, okay, both are fair. In that case, it probably makes sense to drop\nthe \"--dry-run\".\n"},{"id":"476257","messageId":"ZEuJFOBCdnCLdGc5@ncase","threadId":"59619","inReplyTo":"kl6lfs8knc9f.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-04-28T08:51:32Z","receivedAt":"2023-04-28T08:52:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Apr 27, 2023 at 04:20:12PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > On Wed, Apr 26, 2023 at 12:52:46PM -0700, Glen Choo wrote:\n> >> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> > So with that in mind, I'd like to continue ignoring this issue for now\n> > and just not report the remote that the ref came from. But I'd also\n> > argue that we don't have to restrict porcelain mode to single-remote\n> > fetches: it can still be useful to do multi-remote fetches even without\n> > the information where a certain reference update comes from. So any kind\n> > of restriction would feel artificial to me here.\n> >\n> > Furthermore, I'd argue that it is not necessary to label the format as\n> > experimental only because of this limitation. With the refactorings done\n> > in this and the preceding patch series it is easy to add a new format in\n> > case there indeed is somebody that would have a usecase for this. The\n> > \"porcelain\" format should stay stable, and if we decide that we want to\n> > also report the remote for each reference in a follow-up we can easily\n> > add a \"porcelain-v2\" or \"porcelain-with-remote\" format.\n> \n> I agree that the current form is useful, but it seems quite likely that\n> users will want the remote name/url at some point. In the majority of\n> cases, the user can parse the remote name from the updated local ref and\n> if they want, they look up the url by asking \"git config\", but that\n> breaks down quite quickly if they have a custom refspec.\n> \n> My suggestion to label it experimental was meant to give us the freedom\n> to change the \"first version\" in backwards-incompatible ways that we\n> otherwise couldn't. We could say \"here's porcelain-v2, use this and\n> ignore porcelain-v1\", but it would be even nicer if the user didn't need\n> to think about porcelain-v1. The idea is that we could call it\n> experimental, see what users need in the wild, then make changes without\n> having to create a new \"porcelain-v2\" mode.\n> \n> (I realize in hindsight that calling it experimental sounds a bit\n> insulting. I thought your proposal was quite sensible, actually. i\n> apologize if it came across that way)\n\nNo offense taken, so no need to apologize.\n\n> Perhaps part of the reason why you'd prefer not to make it experimental\n> is that users who might want this feature are too afraid of\n> backwards-incompatible changes. I think that's fair.\n\nExactly. I'm implementing this whole feature with a concrete usecase at\nGitLab in mind, and if we declare this as experimental then it will\nbecome a whole lot more risky for us to actually adopt it. Because if\nthe output format may change at any point in time, then the code that\nbuilds on top of it is likely to be broken.\n\n> A different way of\n> achieving the same flexibility would be to make the \"porcelain\" format\n> extensible (like git status --porcelain=v2). A future, multivalued\n> \"--porcelain-options=show-remote-url,show-remote-ref\" would cover\n> our bases.\n\nYeah, that would indeed be quite flexible. Theoretically, there is no\nreason why we couldn't have `--output-format=porcelain,show-remote-url`\nthough.\n\n> I'm happy with either approach as long as we don't have to bikeshed\n> about the \"perfect\" porcelain output :)\n\nAgreed, and that's why I'm currently defending the \"good enough\" format.\nIt should likely work for most usecases that exist out there. The target\naudience is going to be quite small here as this is not a user-directed\nfeature. Furthermore, I assume that the overlap of machines that want to\nparse all reference updates done in a multi-remote fetch while also\nbeing able to exactly tell which remote brought in what updates is going\nto be tiny.\n\n> >> > With these assumptions, the output format becomes unambiguously\n> >> > parseable. Furthermore, given that this output is designed to be\n> >> > consumed by scripts, the machine-readable data is printed to stdout\n> >> > instead of stderr like the human-readable output is. This is mostly done\n> >> > so that other data printed to stderr, like error messages or progress\n> >> > meters, don't interfere with the parseable data.\n> >> \n> >> Sending the 'main output' to stdout makes sense to me, but this (and\n> >> possibly respecting -z) sounds like a different mode of operation, not\n> >> just a matter of formats. It seems different enough that I'd prefer not\n> >> to piggyback on \"fetch.output\" for this (even though this adds more\n> >> surface to the interface...).\n> >> \n> >> We could add --porcelain and say that \"fetch.output\" is ignored if\n> >> --porcelain is also given. That also eliminates the need for\n> >> --output-format, I think.\n> >\n> > I was thinking about this initially, as well. But ultimately I decided\n> > against this especially because of your second paragraph: we'd now need\n> > to think about precedence of options and mutual exclusion, and that to\n> > me feels like an interface that is less obvious than a single knob that\n> > works as you'd expect.\n> \n> My full thoughts on this are in\n> \n>   https://lore.kernel.org/git/kl6lildhlz3i.fsf@chooglen-macbookpro.roam.corp.google.com\n> \n> but the short version is that I'm not sure if I expect something as\n> innocuous-looking as --output-format would imply other, machine-friendly\n> things (like stdout instead of stderr), and using --porcelain might make\n> option precedence clearer in some situtations (like if -z is given).\n\nI'm not even sure that `-z` makes sense in this context. If we see cases\nwhere not using `-z` can cause the machine-readable interface to become\nunparseable then this is a bug in the output format, if you ask me.\nMostly because the whole intent of it is to be machine-parseable. So if\nwe output data that can e.g. contain newlines, then we must not use\nnewlines as part of the output format or alternatively escape them. Why\nlet the author of the script shoot themselves into the foot?\n\nAnyway, I'm digressing. It's hard for me to decide what to do right now.\nThe thread with Junio and Jacob points into the direction of keeping the\n`--output-format=` interface, while this thread points into the other\ndirection. I'm naturally more inclined to keep `--output-format=`,\nmostly because I personally feel like it's the more obvious interface.\nBut I also see your point, so it's not really a choice of right-or-wrong\nhere, but rather of style.\n\nPatrick\n"},{"id":"476277","messageId":"kl6ledo33ovx.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"ZEuJFOBCdnCLdGc5@ncase","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-28T17:20:02Z","receivedAt":"2023-04-28T17:20:09Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> I'm happy with either approach as long as we don't have to bikeshed\n>> about the \"perfect\" porcelain output :)\n>\n> Agreed, and that's why I'm currently defending the \"good enough\" format.\n> It should likely work for most usecases that exist out there. The target\n> audience is going to be quite small here as this is not a user-directed\n> feature.\n\nOkay, I agree that this is \"good enough\" for most cases and we can\nextend it if needed.\n\n>> My full thoughts on this are in\n>> \n>>   https://lore.kernel.org/git/kl6lildhlz3i.fsf@chooglen-macbookpro.roam.corp.google.com\n>> \n>> but the short version is that I'm not sure if I expect something as\n>> innocuous-looking as --output-format would imply other, machine-friendly\n>> things (like stdout instead of stderr), and using --porcelain might make\n>> option precedence clearer in some situtations (like if -z is given).\n>\n> I'm not even sure that `-z` makes sense in this context. If we see cases\n> where not using `-z` can cause the machine-readable interface to become\n> unparseable then this is a bug in the output format, if you ask me.\n> Mostly because the whole intent of it is to be machine-parseable. So if\n> we output data that can e.g. contain newlines, then we must not use\n> newlines as part of the output format or alternatively escape them. Why\n> let the author of the script shoot themselves into the foot?\n\nAgreed. I think the current output format is resilient enough, just that\n\"-z\" is something we choose to support for plumbing anyway.\n\n> Anyway, I'm digressing. It's hard for me to decide what to do right now.\n> The thread with Junio and Jacob points into the direction of keeping the\n> `--output-format=` interface, while this thread points into the other\n> direction. I'm naturally more inclined to keep `--output-format=`,\n> mostly because I personally feel like it's the more obvious interface.\n> But I also see your point, so it's not really a choice of right-or-wrong\n> here, but rather of style.\n\nYes. All sides seem to understand the tradeoffs, but we value them\ndifferently.\n\nI'd personally prefer to err on the side of consistency, because even if\nthe existing behavior is less-than-ideal since:\n\n1) This makes it possible for us to fix it in a consistent, well-thought\n   out way, so we don't have to decide the future for the whole project\n   in a git-fetch series.\n2) At least it still behaves how _some_ users have come to expect it.\n\nAs for which is more consistent, a grep for --porcelain over\nDocumentation/* shows:\n\n- git-push\n- git-status\n- git-worktree\n- git-blame\n- git-commit\n\nMost of them already output to stdout. The lone exception is git-push,\nwhich does exactly this \"use stdout for the main output, but keep using\nstderr for debugging output\" behavior you added, so using --porcelain\nseems somewhat consistent.\n\nOn the other hand, I couldn't find any other option that switches from\nstderr to stdout (we sometimes say --stdout to say \"use stdout instead\nof a file\", but that's different), so if we added this behavior to \"git\nfetch --[output-]format\", this might be the first.\n\n(Sidenote: most instances of --format are for templated-style format,\nbut \"git-replace\" does accept an enum for it. I don't think this should\nweigh heavily in our decision though.)\n\nFor the other, non git-push commands, --porcelain is treated like an\noutput format and respects \"last one wins\" regular option precedence,\ne.g. in the case of \"git status --porcelain --short\". I find this\nbehavior somewhat confusing (I'd expect --porcelain to win, or at least\nbe incompatible), but at least we can consistently change this in the\nfuture [*].\n\nThat said, I'm willing to accept that I'm biased toward my own ideas. If\nothers don't find \"--format=porcelain implies stdout\" confusing, I can\naccept that decision.\n\n[*] In all likelihood, we probably can't make the change to --porcelain\nfor backwards-compatibility reasons, but we could introduce a synonym\nwith the behavior we want (\"--machine\"?) and have OPT_PARSE_MACHINE\nhandle both --porcelain and --machine.\n"},{"id":"476298","messageId":"kl6l8reb3br4.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"xmqq8redhtn6.fsf@gitster.g","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-28T22:03:43Z","receivedAt":"2023-04-28T22:03:53Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I wonder if\n>\n> \tstatic const char *const display_format[] = {\n> \t\t[DISPLAY_FORMAT_UNKNOWN] = NULL,\n> \t\t[DISPLAY_FORMAT_FULL] = \"full\",\n> \t\t[DISPLAY_FORMAT_COMPACT] = \"compact\",\n> \t};\n>\n> would be easier to maintain?\n\nIt's easier to read, so I'd think so.\n"},{"id":"476300","messageId":"kl6l5y9f3ahf.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"0335e5eeb4ded336c5ff7c8888c8aab9dfed2505.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-28T22:31:08Z","receivedAt":"2023-04-28T22:31:14Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> @@ -1894,6 +1902,9 @@ static int fetch_multiple(struct string_list *list, int max_children)\n>  \t\t     \"--no-write-commit-graph\", NULL);\n>  \tadd_options_to_argv(&argv);\n>  \n> +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n> +\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n> +\n\nI think these lines belong inside add_options_to_argv(), since that's\nalso used to prepare argv for fetch_submodules(), so we'd also get\nsupport for --recurse-submodules. (I wish I had spotted that in v1,\nsorry. Thankfully they use the same helper function, so we only have to\ndo this once.)\n\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n  diff --git a/builtin/fetch.c b/builtin/fetch.c\n  index 422e29a914..7aa385aed5 100644\n  --- a/builtin/fetch.c\n  +++ b/builtin/fetch.c\n  @@ -1796,8 +1796,11 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n    return 1;\n  }\n\n  -static void add_options_to_argv(struct strvec *argv)\n  +static void add_options_to_argv(struct strvec *argv,\n  +\t\t\t\tenum display_format format)\n  {\n  /* Maybe this shouldn't be first, idk */\n  +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n  +\t\tstrvec_pushf(argv, \"--output-format=%s\", display_formats[format]);\n    if (dry_run)\n      strvec_push(argv, \"--dry-run\");\n    if (prune != -1)\n  @@ -1908,10 +1911,7 @@ static int fetch_multiple(struct string_list *list, int max_children,\n    strvec_pushl(&argv, \"-c\", \"fetch.bundleURI=\",\n          \"fetch\", \"--append\", \"--no-auto-gc\",\n          \"--no-write-commit-graph\", NULL);\n  -\tadd_options_to_argv(&argv);\n  -\n  -\tif (format != DISPLAY_FORMAT_UNKNOWN)\n  -\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n  +\tadd_options_to_argv(&argv, format);\n\n    if (max_children != 1 && list->nr != 1) {\n      struct parallel_fetch_state state = { argv.v, list, 0, 0 };\n  @@ -2403,7 +2403,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n      if (max_children < 0)\n        max_children = fetch_parallel_config;\n\n  -\t\tadd_options_to_argv(&options);\n  +\t\tadd_options_to_argv(&options, display_format);\n      result = fetch_submodules(the_repository,\n              &options,\n              submodule_prefix,\n\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n\nI tested the result of that locally with --recurse-submodules, and\nit works.\n"},{"id":"476301","messageId":"kl6l354j39yt.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"d7c1bc1a80406ad320c2de684e0c97ba14827c7a.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-04-28T22:42:18Z","receivedAt":"2023-04-28T22:42:23Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> A notable ommission here is that the output format does not include the\n> remote from which a reference was fetched, which might be important\n> information especially in the context of multi-remote fetches. But as\n> such a format would require us to print the remote for every single\n> reference update due to parallelizable fetches it feels wasteful for the\n> most likely usecase, which is when fetching from a single remote. If\n> usecases come up for this in the future though it is easy enough to add\n> a new \"porcelain-v2\" format that adds this information.\n\nWe discussed this elsewhere in the thread, but if we are just adding\ninformation (and not omitting existing information or shuffling it\naround), I would prefer for us to make the format extensible using flags\nthan to add a whole new format enum. We can't imagine what other\ninformation users might want (maybe the remote ref name?), and it would\nbe nice to avoid bumping the 'porcelain version' unnecessarily.\n\nI agree that this format is good enough as a starting point, though.\n\nIt's not new to v2, but I've mentioned my reservations on\n--output-format on:\n\n  https://lore.kernel.org/git/kl6ledo33ovx.fsf@chooglen-macbookpro.roam.corp.google.com/\n\nI will be out of office all of next week. If, in the meantime, everyone\nelse decides that --output-format=porcelain is good enough, I'm happy to\naccept the result.\n\n> @@ -127,6 +193,24 @@ test_expect_success 'fetch output with HEAD and --dry-run' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n> +\ttest_when_finished \"rm -rf head\" &&\n> +\tgit clone . head &&\n> +\tCOMMIT_ID=$(git rev-parse HEAD) &&\n> +\n> +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n> +\tcat >expect <<-EOF &&\n> +\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\n> +\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n> +\tcat >expect <<-EOF &&\n> +\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n\nNow that the earlier test also tests without --dry-run, shouldn't this\none too?\n\n"},{"id":"476305","messageId":"20230429173448.GD3271@szeder.dev","threadId":"59619","inReplyTo":"0d0d50d14c557f8313747e7d0e104c2c0819dab9.1682593865.git.ps@pks.im","subject":"Re: [PATCH v2 1/8] fetch: split out tests for output format","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2023-04-29T17:34:48Z","receivedAt":"2023-04-29T17:35:06Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Thu, Apr 27, 2023 at 01:13:08PM +0200, Patrick Steinhardt wrote:\n> We're about to introduce a new porcelain mode for the output of\n> git-fetch(1). As part of that we'll be introducing a set of new tests\n> that only relate to the output of this command.\n> \n> Split out tests that exercise the output format of git-fetch(1) so that\n> it becomes easier to verify this functionality as a standalone unit. As\n> the tests assume that the default branch is called \"main\" we set up the\n> corresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\n> accordingly.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t5510-fetch.sh        | 53 ----------------------------------\n>  t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 63 insertions(+), 53 deletions(-)\n>  create mode 100755 t/t5574-fetch-output.sh\n\n\n> diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> new file mode 100755\n> index 0000000000..f91b654d38\n> --- /dev/null\n> +++ b/t/t5574-fetch-output.sh\n> @@ -0,0 +1,63 @@\n> +#!/bin/sh\n> +\n> +test_description='git fetch output format'\n> +\n> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'fetch aligned output' '\n> +\tgit clone . full-output &&\n> +\ttest_commit looooooooooooong-tag &&\n> +\t(\n> +\t\tcd full-output &&\n> +\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n\nWhy is that 2>&1 redirection used here?\nIf the output the test case is interested in goes to the command's\nstandard output, then it's unnecessary.  However, if it goes to\nstandard error, then why is standard output redirected as well?\n\nI understand that this patch just moves existing test cases around\nas-is, so this is not something you introduced, but I point it out\nhere, because later patches of this series add several new test cases\nfollowing this anti-pattern.\n\nSince this series creates a new test script, perhaps this might be the\nright time to clean this up.\n\n> +\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n> +\t) &&\n> +\tcat >expect <<-\\EOF &&\n> +\tmain                 -> origin/main\n> +\tlooooooooooooong-tag -> looooooooooooong-tag\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'fetch compact output' '\n> +\tgit clone . compact &&\n> +\ttest_commit extraaa &&\n> +\t(\n> +\t\tcd compact &&\n> +\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n> +\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n> +\t) &&\n> +\tcat >expect <<-\\EOF &&\n> +\tmain       -> origin/*\n> +\textraaa    -> *\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '--no-show-forced-updates' '\n> +\tmkdir forced-updates &&\n> +\t(\n> +\t\tcd forced-updates &&\n> +\t\tgit init &&\n> +\t\ttest_commit 1 &&\n> +\t\ttest_commit 2\n> +\t) &&\n> +\tgit clone forced-updates forced-update-clone &&\n> +\tgit clone forced-updates no-forced-update-clone &&\n> +\tgit -C forced-updates reset --hard HEAD~1 &&\n> +\t(\n> +\t\tcd forced-update-clone &&\n> +\t\tgit fetch --show-forced-updates origin 2>output &&\n\nOh, look, there are some good examples to follow here as well :)\n\n> +\t\ttest_i18ngrep \"(forced update)\" output\n> +\t) &&\n> +\t(\n> +\t\tcd no-forced-update-clone &&\n> +\t\tgit fetch --no-show-forced-updates origin 2>output &&\n> +\t\ttest_i18ngrep ! \"(forced update)\" output\n> +\t)\n> +'\n> +\n> +test_done\n> -- \n> 2.40.1\n> \n\n\n"},{"id":"476458","messageId":"645178df26120_200ae294e1@chronos.notmuch","threadId":"59619","inReplyTo":"ZEpVSrz-uUcfN_3_@ncase","subject":"Re: [PATCH 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2023-05-02T20:55:59Z","receivedAt":"2023-05-02T20:56:06Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Patrick Steinhardt wrote:\n> On Wed, Apr 26, 2023 at 12:52:46PM -0700, Glen Choo wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > \n> > > The output format is quite simple:\n> > >\n> > > ```\n> > > <flag> <old-object-id> <new-object-id> <local-reference>\\n\n> > > ```\n> > \n> > This format doesn't show the remote name or url that was fetched. That\n> > seems okay when fetching with a single remote, but it seems necessary\n> > with \"--all\". Perhaps you were planning to add that in a later series?\n> > If so, I think it's okay to call the \"porcelain\" format experimental,\n> > and forbid porcelain + --all until then.\n> \n> The reason is mostly that I didn't find an output format that I really\n> liked here. We'd basically have to repeat the remote URL for every\n> single reference: just repeating it once per remote doesn't fly because\n> with `--parallel` the output could be intermingled. But doing that feels\n> wasteful to me, so I bailed. I guess I'm also biased here because it\n> just wouldn't be useful to myself.\n\nCouldn't each URL be assigned an index and then just show `0` in the output? I\nthink it should be straightforward for a parser to figure out what `0` means\nbut if not, that's something that could be figured out once the use case\narrives (which very well could be never).\n\n-- \nFelipe Contreras\n"},{"id":"476495","messageId":"ZFIll22dBQvSm2nz@ncase","threadId":"59619","inReplyTo":"kl6l8reb3br4.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T09:12:55Z","receivedAt":"2023-05-03T09:14:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Apr 28, 2023 at 03:03:43PM -0700, Glen Choo wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > I wonder if\n> >\n> > \tstatic const char *const display_format[] = {\n> > \t\t[DISPLAY_FORMAT_UNKNOWN] = NULL,\n> > \t\t[DISPLAY_FORMAT_FULL] = \"full\",\n> > \t\t[DISPLAY_FORMAT_COMPACT] = \"compact\",\n> > \t};\n> >\n> > would be easier to maintain?\n> \n> It's easier to read, so I'd think so.\n\nYeah, I'll adopt this approach in v3.\n\nPatrick\n"},{"id":"476496","messageId":"ZFIsw94SBxRVkWmz@ncase","threadId":"59619","inReplyTo":"kl6l5y9f3ahf.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T09:43:31Z","receivedAt":"2023-05-03T09:44:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Apr 28, 2023 at 03:31:08PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > @@ -1894,6 +1902,9 @@ static int fetch_multiple(struct string_list *list, int max_children)\n> >  \t\t     \"--no-write-commit-graph\", NULL);\n> >  \tadd_options_to_argv(&argv);\n> >  \n> > +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n> > +\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n> > +\n> \n> I think these lines belong inside add_options_to_argv(), since that's\n> also used to prepare argv for fetch_submodules(), so we'd also get\n> support for --recurse-submodules. (I wish I had spotted that in v1,\n> sorry. Thankfully they use the same helper function, so we only have to\n> do this once.)\n> \n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>   diff --git a/builtin/fetch.c b/builtin/fetch.c\n>   index 422e29a914..7aa385aed5 100644\n>   --- a/builtin/fetch.c\n>   +++ b/builtin/fetch.c\n>   @@ -1796,8 +1796,11 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n>     return 1;\n>   }\n> \n>   -static void add_options_to_argv(struct strvec *argv)\n>   +static void add_options_to_argv(struct strvec *argv,\n>   +\t\t\t\tenum display_format format)\n>   {\n>   /* Maybe this shouldn't be first, idk */\n>   +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n>   +\t\tstrvec_pushf(argv, \"--output-format=%s\", display_formats[format]);\n>     if (dry_run)\n>       strvec_push(argv, \"--dry-run\");\n>     if (prune != -1)\n>   @@ -1908,10 +1911,7 @@ static int fetch_multiple(struct string_list *list, int max_children,\n>     strvec_pushl(&argv, \"-c\", \"fetch.bundleURI=\",\n>           \"fetch\", \"--append\", \"--no-auto-gc\",\n>           \"--no-write-commit-graph\", NULL);\n>   -\tadd_options_to_argv(&argv);\n>   -\n>   -\tif (format != DISPLAY_FORMAT_UNKNOWN)\n>   -\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n>   +\tadd_options_to_argv(&argv, format);\n> \n>     if (max_children != 1 && list->nr != 1) {\n>       struct parallel_fetch_state state = { argv.v, list, 0, 0 };\n>   @@ -2403,7 +2403,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n>       if (max_children < 0)\n>         max_children = fetch_parallel_config;\n> \n>   -\t\tadd_options_to_argv(&options);\n>   +\t\tadd_options_to_argv(&options, display_format);\n>       result = fetch_submodules(the_repository,\n>               &options,\n>               submodule_prefix,\n> \n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n> \n> I tested the result of that locally with --recurse-submodules, and\n> it works.\n\nUnfortunately it doesn't quite work alright: while the porcelain format\ndoes indeed get inherited to the child process correctly, the parallel\nprocess API will cause us to group output per submodule-fetch. This has\nthe consequence that stdout will be redirected into stderr, and that\nthen breaks the assumption that all machine-parseable output goes to\nstdout.\n\nMy initial reflex is to just outright reject porcelain mode when\nsubmodule fetches are enabled. But that would require the caller to\nalways explicitly pass `--recurse-submodules=off`, which isn't exactly\ngreat usability-wise.\n\nThe alternative would be to ungroup the output so that we can continue\nto print to the correct output streams. That works alright, and I've got\na working version that does exactly that. But now we have the issue that\nthe porcelain output is misleading: you cannot tell whether a specific\nreference update happens in the parent repository or in the submodule as\nthat information is not part of the output.\n\nI consider the second option to be much worse than the first option\nbecause it can cause scripts do to the wrong thing. So I'll send v3 with\nthe first option, even though it's kind of an awful workaround. I'd be\nhappy to hear any alternative proposals though.\n\nPatrick\n"},{"id":"476497","messageId":"ZFJDqXnonTKIejAt@ncase","threadId":"59619","inReplyTo":"20230429173448.GD3271@szeder.dev","subject":"Re: [PATCH v2 1/8] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:21:13Z","receivedAt":"2023-05-03T11:21:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Apr 29, 2023 at 07:34:48PM +0200, SZEDER Gábor wrote:\n> On Thu, Apr 27, 2023 at 01:13:08PM +0200, Patrick Steinhardt wrote:\n> > We're about to introduce a new porcelain mode for the output of\n> > git-fetch(1). As part of that we'll be introducing a set of new tests\n> > that only relate to the output of this command.\n> > \n> > Split out tests that exercise the output format of git-fetch(1) so that\n> > it becomes easier to verify this functionality as a standalone unit. As\n> > the tests assume that the default branch is called \"main\" we set up the\n> > corresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\n> > accordingly.\n> > \n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  t/t5510-fetch.sh        | 53 ----------------------------------\n> >  t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n> >  2 files changed, 63 insertions(+), 53 deletions(-)\n> >  create mode 100755 t/t5574-fetch-output.sh\n> \n> \n> > diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> > new file mode 100755\n> > index 0000000000..f91b654d38\n> > --- /dev/null\n> > +++ b/t/t5574-fetch-output.sh\n> > @@ -0,0 +1,63 @@\n> > +#!/bin/sh\n> > +\n> > +test_description='git fetch output format'\n> > +\n> > +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> > +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +test_expect_success 'fetch aligned output' '\n> > +\tgit clone . full-output &&\n> > +\ttest_commit looooooooooooong-tag &&\n> > +\t(\n> > +\t\tcd full-output &&\n> > +\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n> \n> Why is that 2>&1 redirection used here?\n> If the output the test case is interested in goes to the command's\n> standard output, then it's unnecessary.  However, if it goes to\n> standard error, then why is standard output redirected as well?\n> \n> I understand that this patch just moves existing test cases around\n> as-is, so this is not something you introduced, but I point it out\n> here, because later patches of this series add several new test cases\n> following this anti-pattern.\n> \n> Since this series creates a new test script, perhaps this might be the\n> right time to clean this up.\n\nI feel like this patch series is big enough on its own already, so for\nnow I'd like to refrain from touching up the old tests. But you've got a\ngood point here, and I'll make sure that any new tests don't followw the\nsame pattern.\n\nThat being said, I think we should keep on testing both stdout and\nstderr. The tests here are explicitly about the output format, so it\ndoes make sense to verify that both of them match what we expect. We\nshould treat them as separate things though.\n\nPatrick\n"},{"id":"476498","messageId":"cover.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:14Z","receivedAt":"2023-05-03T11:34:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the third version of my patch series to introduce a\nmachine-parseable output for git-fetch(1).\n\nChanges compared to v2:\n\n    - A new commit that fixes `--no-recurse-submodules` not being\n      honored for multi-remote fetches was added. This fixes issues that\n      would otherwise be hit later in the patch series.\n\n    - Tests were adjusted to explicitly verify stdout and stderr as\n      standalone units instead of redirecting 2>&1.\n\n    - Instead of introducing `--output-format` and a new \"porcelain\"\n      format I've changed this to instead introduce `--porcelain`. What\n      finally convinced me to go this way is Glen's list of preexisting\n      commands that have `--porcelain`. So I decided to not be the only\n      outlier here.\n\n    - The DISPLAY_FORMAT_MAX macro was dropped in favor of Junio's\n      proposal that uses designated initializers for the array.\n\n    - Similar to `--negotiate-only`, the porcelain output is now\n      incompatible with `--recurse-submodules=[yes|on-demand]`. This is\n      done to avoid ambiguity when references in both the superproject\n      and any of its submodules is updated.\n\nThanks again for all the feedback!\n\nPatrick\n\nPatrick Steinhardt (8):\n  fetch: fix `--no-recurse-submodules` with multi-remote fetches\n  fetch: split out tests for output format\n  fetch: add a test to exercise invalid output formats\n  fetch: fix missing from-reference when fetching HEAD:foo\n  fetch: introduce `display_format` enum\n  fetch: move display format parsing into main function\n  fetch: move option related variables into main function\n  fetch: introduce machine-parseable \"porcelain\" output format\n\n Documentation/fetch-options.txt |   6 +\n Documentation/git-fetch.txt     |   9 +\n builtin/fetch.c                 | 437 +++++++++++++++++++-------------\n t/t5510-fetch.sh                |  53 ----\n t/t5526-fetch-submodules.sh     |  31 +++\n t/t5574-fetch-output.sh         | 231 +++++++++++++++++\n 6 files changed, 543 insertions(+), 224 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\nRange-diff against v2:\n-:  ---------- > 1:  4b2b0cfe15 fetch: fix `--no-recurse-submodules` with multi-remote fetches\n1:  0d0d50d14c = 2:  6ebc7450ba fetch: split out tests for output format\n2:  29d2c58914 ! 3:  78479922ac fetch: add a test to exercise invalid output formats\n    @@ t/t5574-fetch-output.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +\ttest_when_finished \"rm -rf clone\" &&\n     +\tgit clone . clone &&\n     +\n    -+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual 2>&1 &&\n    ++\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual.out 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n     +\tEOF\n    -+\ttest_cmp expect actual &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err &&\n     +\n    -+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual 2>&1 &&\n    ++\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual.out 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n     +\tEOF\n    -+\ttest_cmp expect actual\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err\n     +'\n     +\n      test_expect_success 'fetch aligned output' '\n3:  d1fb6eeae7 ! 4:  46e1266ab0 fetch: fix missing from-reference when fetching HEAD:foo\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n      \ttest_cmp expect actual\n      '\n      \n    -+test_expect_success 'fetch output with HEAD and --dry-run' '\n    ++test_expect_success 'fetch output with HEAD' '\n     +\ttest_when_finished \"rm -rf head\" &&\n     +\tgit clone . head &&\n     +\n    -+\tgit -C head fetch --dry-run origin HEAD >actual 2>&1 &&\n    ++\tgit -C head fetch --dry-run origin HEAD >actual.out 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tFrom $(test-tool path-utils real_path .)/.\n     +\t * branch            HEAD       -> FETCH_HEAD\n     +\tEOF\n    -+\ttest_cmp expect actual &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err &&\n     +\n    -+\tgit -C head fetch origin HEAD >actual 2>&1 &&\n    -+\ttest_cmp expect actual &&\n    ++\tgit -C head fetch origin HEAD >actual.out 2>actual.err &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err &&\n     +\n    -+\tgit -C head fetch --dry-run origin HEAD:foo >actual 2>&1 &&\n    ++\tgit -C head fetch --dry-run origin HEAD:foo >actual.out 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tFrom $(test-tool path-utils real_path .)/.\n     +\t * [new ref]         HEAD       -> foo\n     +\tEOF\n    -+\ttest_cmp expect actual &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err &&\n     +\n    -+\tgit -C head fetch origin HEAD:foo >actual 2>&1 &&\n    -+\ttest_cmp expect actual\n    ++\tgit -C head fetch origin HEAD:foo >actual.out 2>actual.err &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err\n     +'\n     +\n      test_expect_success '--no-show-forced-updates' '\n4:  b545bf8bb9 ! 5:  acc0f7f520 fetch: introduce `display_format` enum\n    @@ builtin/fetch.c: static void display_state_init(struct display_state *display_st\n     +\t\tbreak;\n     +\t}\n     +\tdefault:\n    -+\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n    ++\t\tBUG(\"unexpected display format %d\", display_state->format);\n      \t}\n      }\n      \n5:  4990d35998 = 6:  cd23440128 fetch: move display format parsing into main function\n6:  cfe84129ab = 7:  edbc31013f fetch: move option related variables into main function\n7:  0335e5eeb4 < -:  ---------- fetch: introduce new `--output-format` option\n8:  d7c1bc1a80 ! 8:  e132d9494e fetch: introduce machine-parseable \"porcelain\" output format\n    @@ Commit message\n         information especially in the context of multi-remote fetches. But as\n         such a format would require us to print the remote for every single\n         reference update due to parallelizable fetches it feels wasteful for the\n    -    most likely usecase, which is when fetching from a single remote. If\n    -    usecases come up for this in the future though it is easy enough to add\n    -    a new \"porcelain-v2\" format that adds this information.\n    +    most likely usecase, which is when fetching from a single remote.\n    +\n    +    In a similar spirit, a second restriction is that this cannot be used\n    +    with `--recurse-submodules`. This is because any reference updates would\n    +    be ambiguous without also printing the repository in which the update\n    +    happens.\n    +\n    +    Considering that both multi-remote and submodule fetches are rather\n    +    niche and likely not going to be useful for the majority of usecases\n    +    these omissions feel acceptable. If usecases for either of these come up\n    +    in the future though it is easy enough to add a new \"porcelain-v2\"\n    +    format that adds this information.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    - ## Documentation/config/fetch.txt ##\n    -@@ Documentation/config/fetch.txt: fetch.pruneTags::\n    - \n    - fetch.output::\n    - \tControl how ref update status is printed. Valid values are\n    --\t`full` and `compact`. Default value is `full`. See section\n    --\tOUTPUT in linkgit:git-fetch[1] for detail.\n    -+\t`full`, `compact` and `porcelain`. Default value is `full`.\n    -+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n    - \n    - fetch.negotiationAlgorithm::\n    - \tControl how information about the commits in the local repository\n    -\n      ## Documentation/fetch-options.txt ##\n     @@ Documentation/fetch-options.txt: linkgit:git-config[1].\n    + --dry-run::\n    + \tShow what would be done, without making any changes.\n      \n    - --output-format::\n    - \tControl how ref update status is printed. Valid values are\n    --\t`full` and `compact`. Default value is `full`. See section\n    --\tOUTPUT in linkgit:git-fetch[1] for detail.\n    -+\t`full`, `compact` and `porcelain`. Default value is `full`.\n    -+\tSee section OUTPUT in linkgit:git-fetch[1] for detail.\n    - \n    ++--porcelain::\n    ++\tPrint the output to standard output in an easy-to-parse format for\n    ++\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n    +++\n    ++This is incompatible with `--recurse-submodules=[yes|on-demand]`.\n    ++\n      ifndef::git-pull[]\n      --[no-]write-fetch-head::\n    + \tWrite the list of remote refs fetched in the `FETCH_HEAD`\n     \n      ## Documentation/git-fetch.txt ##\n    -@@ Documentation/git-fetch.txt: The output of \"git fetch\" depends on the transport method used; this\n    - section describes the output when fetching over the Git protocol\n    - (either locally or via ssh) and Smart HTTP protocol.\n    - \n    --The status of the fetch is output in tabular form, with each line\n    --representing the status of a single ref. Each line is of the form:\n    -+The output format can be chosen either via the `fetch.output` config\n    -+(see linkgit:git-config[1]), or via the `--output-format` switch.\n    -+Supported values include:\n    -+\n    -+For the `full` and `compact` output formats, the status of the fetch is\n    -+output in tabular, with each line representing the status of a single\n    -+ref. Each line is of the form:\n    - \n    - -------------------------------\n    +@@ Documentation/git-fetch.txt: representing the status of a single ref. Each line is of the form:\n       <flag> <summary> <from> -> <to> [<reason>]\n      -------------------------------\n      \n    -+The `porcelain` output format is intended to be machine-parseable. In\n    -+contrast to the human-readable output formats it thus prints to standard\n    -+output instead of standard error. Each line is of the form:\n    ++When using `--porcelain`, the output format is intended to be\n    ++machine-parseable. In contrast to the human-readable output formats it\n    ++thus prints to standard output instead of standard error. Each line is\n    ++of the form:\n     +\n     +-------------------------------\n     +<flag> <old-object-id> <new-object-id> <local-reference>\n    @@ builtin/fetch.c: enum display_format {\n      \tDISPLAY_FORMAT_FULL,\n      \tDISPLAY_FORMAT_COMPACT,\n     +\tDISPLAY_FORMAT_PORCELAIN,\n    - \tDISPLAY_FORMAT_MAX,\n    - };\n    - \n    -@@ builtin/fetch.c: static const char * const display_formats[DISPLAY_FORMAT_MAX] = {\n    - \tNULL,\n    - \t\"full\",\n    - \t\"compact\",\n    -+\t\"porcelain\",\n      };\n      \n      struct display_state {\n    @@ builtin/fetch.c: static void display_state_init(struct display_state *display_st\n     +\t\t/* We don't need to precompute anything here. */\n     +\t\tbreak;\n      \tdefault:\n    --\t\tBUG(\"unexpected display foramt %d\", display_state->format);\n    -+\t\tBUG(\"unexpected display format %d\", display_state->format);\n    + \t\tBUG(\"unexpected display format %d\", display_state->format);\n      \t}\n    - }\n    - \n     @@ builtin/fetch.c: static void print_compact(struct display_state *display_state,\n      static void display_ref_update(struct display_state *display_state, char code,\n      \t\t\t       const char *summary, const char *error,\n    @@ builtin/fetch.c: static int prune_refs(struct display_state *display_state,\n      \t\t\t\t\t   summary_width);\n      \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n      \t\t}\n    +@@ builtin/fetch.c: static int add_remote_or_group(const char *name, struct string_list *list)\n    + \treturn 1;\n    + }\n    + \n    +-static void add_options_to_argv(struct strvec *argv)\n    ++static void add_options_to_argv(struct strvec *argv,\n    ++\t\t\t\tenum display_format format)\n    + {\n    + \tif (dry_run)\n    + \t\tstrvec_push(argv, \"--dry-run\");\n    +@@ builtin/fetch.c: static void add_options_to_argv(struct strvec *argv)\n    + \t\tstrvec_push(argv, \"--ipv6\");\n    + \tif (!write_fetch_head)\n    + \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n    ++\tif (format == DISPLAY_FORMAT_PORCELAIN)\n    ++\t\tstrvec_pushf(argv, \"--porcelain\");\n    + }\n    + \n    + /* Fetch multiple remotes in parallel */\n    +@@ builtin/fetch.c: struct parallel_fetch_state {\n    + \tconst char **argv;\n    + \tstruct string_list *remotes;\n    + \tint next, result;\n    ++\tenum display_format format;\n    + };\n    + \n    + static int fetch_next_remote(struct child_process *cp,\n    +@@ builtin/fetch.c: static int fetch_next_remote(struct child_process *cp,\n    + \tstrvec_push(&cp->args, remote);\n    + \tcp->git_cmd = 1;\n    + \n    +-\tif (verbosity >= 0)\n    ++\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n    + \t\tprintf(_(\"Fetching %s\\n\"), remote);\n    + \n    + \treturn 1;\n    +@@ builtin/fetch.c: static int fetch_finished(int result, struct strbuf *out,\n    + \treturn 0;\n    + }\n    + \n    +-static int fetch_multiple(struct string_list *list, int max_children)\n    ++static int fetch_multiple(struct string_list *list, int max_children,\n    ++\t\t\t  enum display_format format)\n    + {\n    + \tint i, result = 0;\n    + \tstruct strvec argv = STRVEC_INIT;\n    +@@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n    + \n    + \tstrvec_pushl(&argv, \"fetch\", \"--append\", \"--no-auto-gc\",\n    + \t\t     \"--no-write-commit-graph\", NULL);\n    +-\tadd_options_to_argv(&argv);\n    ++\tadd_options_to_argv(&argv, format);\n    + \n    + \tif (max_children != 1 && list->nr != 1) {\n    +-\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n    ++\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0, format };\n    + \t\tconst struct run_process_parallel_opts opts = {\n    + \t\t\t.tr2_category = \"fetch\",\n    + \t\t\t.tr2_label = \"parallel/fetch\",\n    +@@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n    + \n    + \t\t\tstrvec_pushv(&cmd.args, argv.v);\n    + \t\t\tstrvec_push(&cmd.args, name);\n    +-\t\t\tif (verbosity >= 0)\n    ++\t\t\tif (verbosity >= 0 && format != DISPLAY_FORMAT_PORCELAIN)\n    + \t\t\t\tprintf(_(\"Fetching %s\\n\"), name);\n    + \t\t\tcmd.git_cmd = 1;\n    + \t\t\tif (run_command(&cmd)) {\n    +@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,\n    + \treturn exit_code;\n    + }\n    + \n    ++static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n    ++{\n    ++\tenum display_format *format = opt->value;\n    ++\t*format = DISPLAY_FORMAT_PORCELAIN;\n    ++\treturn 0;\n    ++}\n    ++\n    + int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + {\n    + \tconst char *bundle_uri;\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n    + \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n    + \t\t\t N_(\"dry run\")),\n    ++\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n    ++\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n    + \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n    + \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n    + \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\tfetch_config_from_gitmodules(sfjc, rs);\n    + \t}\n    + \n    ++\tif (display_format == DISPLAY_FORMAT_PORCELAIN) {\n    ++\t\tswitch (recurse_submodules_cli) {\n    ++\t\tcase RECURSE_SUBMODULES_OFF:\n    ++\t\tcase RECURSE_SUBMODULES_DEFAULT:\n    ++\t\t\t/*\n    ++\t\t\t * Reference updates in submodules would be ambiguous\n    ++\t\t\t * in porcelain mode, so we reject this combination.\n    ++\t\t\t */\n    ++\t\t\trecurse_submodules = RECURSE_SUBMODULES_OFF;\n    ++\t\t\tbreak;\n    ++\n    ++\t\tdefault:\n    ++\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n    ++\t\t\t    \"--porcelain\", \"--recurse-submodules\");\n    ++\t\t}\n    ++\t}\n    ++\n    + \tif (negotiate_only && !negotiation_tip.nr)\n    + \t\tdie(_(\"--negotiate-only needs one or more --negotiation-tip=*\"));\n    + \n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\t\tmax_children = fetch_parallel_config;\n    + \n    + \t\t/* TODO should this also die if we have a previous partial-clone? */\n    +-\t\tresult = fetch_multiple(&list, max_children);\n    ++\t\tresult = fetch_multiple(&list, max_children, display_format);\n    + \t}\n    + \n    + \n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \t\tif (max_children < 0)\n    + \t\t\tmax_children = fetch_parallel_config;\n    + \n    +-\t\tadd_options_to_argv(&options);\n    ++\t\tadd_options_to_argv(&options, display_format);\n    + \t\tresult = fetch_submodules(the_repository,\n    + \t\t\t\t\t  &options,\n    + \t\t\t\t\t  submodule_prefix,\n     \n      ## t/t5574-fetch-output.sh ##\n    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output with multiple remotes' '\n    +@@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n      \ttest_cmp expect actual\n      '\n      \n     +test_expect_success 'fetch porcelain output' '\n    -+\ttest_when_finished \"rm -rf porcelain-cfg porcelain-cli\" &&\n    ++\ttest_when_finished \"rm -rf porcelain\" &&\n     +\n     +\t# Set up a bunch of references that we can use to demonstrate different\n     +\t# kinds of flag symbols in the output format.\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output with multiple\n     +\t# namespaces so that we can test that rejected and force-updated\n     +\t# references are reported properly.\n     +\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n    -+\tgit clone . porcelain-cli &&\n    -+\tgit clone . porcelain-cfg &&\n    -+\tgit -C porcelain-cfg fetch origin $refspecs &&\n    -+\tgit -C porcelain-cli fetch origin $refspecs &&\n    ++\tgit clone . porcelain &&\n    ++\tgit -C porcelain fetch origin $refspecs &&\n     +\n     +\t# Now that we have set up the client repositories we can change our\n     +\t# local references.\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output with multiple\n     +\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n     +\t# and non-dry-run fetches produces the same output. Execution of the\n     +\t# fetch is expected to fail as we have a rejected reference update.\n    -+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --dry-run --prune origin $refspecs >actual-dry-run-cfg &&\n    -+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --dry-run --prune origin $refspecs >actual-dry-run-cli &&\n    -+\ttest_cmp actual-dry-run-cfg actual-dry-run-cli &&\n    -+\ttest_cmp expect actual-dry-run-cfg &&\n    ++\ttest_must_fail git -C porcelain fetch \\\n    ++\t\t--porcelain --dry-run --prune origin $refspecs >actual &&\n    ++\ttest_cmp expect actual &&\n     +\n     +\t# And now we perform a non-dry-run fetch.\n    -+\ttest_must_fail git -C porcelain-cfg -c fetch.output=porcelain fetch --prune origin $refspecs >actual-cfg &&\n    -+\ttest_must_fail git -C porcelain-cli fetch --output-format=porcelain --prune origin $refspecs >actual-cli &&\n    -+\ttest_cmp actual-cfg actual-cli &&\n    -+\ttest_cmp expect actual-cfg &&\n    -+\n    -+\t# Ensure that the dry-run and non-dry-run output matches.\n    -+\ttest_cmp actual-dry-run-cfg actual-cfg\n    ++\ttest_must_fail git -C porcelain fetch \\\n    ++\t\t--porcelain --prune origin $refspecs >actual 2>stderr &&\n    ++\ttest_cmp expect actual &&\n    ++\ttest_must_be_empty stderr\n     +'\n     +\n    - test_expect_success 'fetch output with HEAD and --dry-run' '\n    ++test_expect_success 'fetch porcelain with multiple remotes' '\n    ++\ttest_when_finished \"rm -rf porcelain\" &&\n    ++\n    ++\tgit clone . porcelain &&\n    ++\tgit -C porcelain remote add second-remote \"$PWD\" &&\n    ++\tgit -C porcelain fetch second-remote &&\n    ++\n    ++\ttest_commit --no-tag multi-commit &&\n    ++\told_commit=$(git rev-parse HEAD~) &&\n    ++\tnew_commit=$(git rev-parse HEAD) &&\n    ++\n    ++\tcat >expect <<-EOF &&\n    ++\t  $old_commit $new_commit refs/remotes/origin/force-updated\n    ++\t  $old_commit $new_commit refs/remotes/second-remote/force-updated\n    ++\tEOF\n    ++\n    ++\tgit -C porcelain fetch --porcelain --all >actual 2>stderr &&\n    ++\ttest_cmp expect actual &&\n    ++\ttest_must_be_empty stderr\n    ++'\n    ++\n    ++test_expect_success 'fetch porcelain refuses to work with submodules' '\n    ++\ttest_when_finished \"rm -rf porcelain\" &&\n    ++\n    ++\tcat >expect <<-EOF &&\n    ++\tfatal: options ${SQ}--porcelain${SQ} and ${SQ}--recurse-submodules${SQ} cannot be used together\n    ++\tEOF\n    ++\n    ++\tgit init porcelain &&\n    ++\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=yes 2>stderr &&\n    ++\ttest_cmp expect stderr &&\n    ++\n    ++\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=on-demand 2>stderr &&\n    ++\ttest_cmp expect stderr\n    ++'\n    ++\n    + test_expect_success 'fetch output with HEAD' '\n      \ttest_when_finished \"rm -rf head\" &&\n      \tgit clone . head &&\n    -@@ t/t5574-fetch-output.sh: test_expect_success 'fetch output with HEAD and --dry-run' '\n    - \ttest_cmp expect actual\n    +@@ t/t5574-fetch-output.sh: test_expect_success 'fetch output with HEAD' '\n    + \ttest_cmp expect actual.err\n      '\n      \n    -+test_expect_success 'fetch porcelain output with HEAD and --dry-run' '\n    ++test_expect_success 'fetch porcelain output with HEAD' '\n     +\ttest_when_finished \"rm -rf head\" &&\n     +\tgit clone . head &&\n     +\tCOMMIT_ID=$(git rev-parse HEAD) &&\n     +\n    -+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD >actual &&\n    ++\tgit -C head fetch --porcelain --dry-run origin HEAD >actual &&\n     +\tcat >expect <<-EOF &&\n     +\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n     +\tEOF\n     +\ttest_cmp expect actual &&\n     +\n    -+\tgit -C head fetch --output-format=porcelain --dry-run origin HEAD:foo >actual &&\n    ++\tgit -C head fetch --porcelain origin HEAD >actual &&\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tgit -C head fetch --porcelain --dry-run origin HEAD:foo >actual &&\n     +\tcat >expect <<-EOF &&\n     +\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n     +\tEOF\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tgit -C head fetch --porcelain origin HEAD:foo >actual &&\n     +\ttest_cmp expect actual\n     +'\n     +\n-- \n2.40.1\n\n"},{"id":"476499","messageId":"4b2b0cfe15ca6b3c9058bc9cb6944ba05d60a228.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:18Z","receivedAt":"2023-05-03T11:34:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When running `git fetch --no-recurse-submodules`, the exectation is that\nwe don't fetch any submodules. And while this works for fetches of a\nsingle remote, it doesn't when fetching multiple remotes at once. The\nresult is that we do recurse into submodules even though the user has\nexplicitly asked us not to.\n\nThis is because while we pass on `--recurse-submodules={yes,on-demand}`\nif specified by the user, we don't pass on `--no-recurse-submodules` to\nthe subprocess spawned to perform the submodule fetch.\n\nFix this by also forwarding this flag as expected.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c             |  2 ++\n t/t5526-fetch-submodules.sh | 31 +++++++++++++++++++++++++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c310d89878..08d7fc7233 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1876,6 +1876,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--keep\");\n \tif (recurse_submodules == RECURSE_SUBMODULES_ON)\n \t\tstrvec_push(argv, \"--recurse-submodules\");\n+\telse if (recurse_submodules == RECURSE_SUBMODULES_OFF)\n+\t\tstrvec_push(argv, \"--no-recurse-submodules\");\n \telse if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)\n \t\tstrvec_push(argv, \"--recurse-submodules=on-demand\");\n \tif (tags == TAGS_SET)\ndiff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\nindex dcdbe26a08..162e5bac2f 100755\n--- a/t/t5526-fetch-submodules.sh\n+++ b/t/t5526-fetch-submodules.sh\n@@ -1180,4 +1180,35 @@ test_expect_success 'fetch --all with --recurse-submodules with multiple' '\n \ttest_line_count = 2 fetch-subs\n '\n \n+test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n+\ttest_when_finished \"git -C downstream remote remove second\" &&\n+\n+\t# We need to add a second remote, otherwise --all falls back to the\n+\t# normal fetch-one case.\n+\tgit -C downstream remote add second .. &&\n+\tgit -C downstream fetch --all &&\n+\n+\tadd_submodule_commits &&\n+\tadd_superproject_commits &&\n+\told_commit=$(git rev-parse --short HEAD~) &&\n+\tnew_commit=$(git rev-parse --short HEAD) &&\n+\n+\tgit -C downstream fetch --all --no-recurse-submodules >actual.out 2>actual.err &&\n+\n+\tcat >expect.out <<-EOF &&\n+\tFetching origin\n+\tFetching second\n+\tEOF\n+\n+\tcat >expect.err <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t   $old_commit..$new_commit  super      -> origin/super\n+\tFrom ..\n+\t   $old_commit..$new_commit  super      -> second/super\n+\tEOF\n+\n+\ttest_cmp expect.out actual.out &&\n+\ttest_cmp expect.err actual.err\n+'\n+\n test_done\n-- \n2.40.1\n\n"},{"id":"476500","messageId":"6ebc7450bab51083ae225cad768a013e282749a2.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 2/8] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:22Z","receivedAt":"2023-05-03T11:34:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new porcelain mode for the output of\ngit-fetch(1). As part of that we'll be introducing a set of new tests\nthat only relate to the output of this command.\n\nSplit out tests that exercise the output format of git-fetch(1) so that\nit becomes easier to verify this functionality as a standalone unit. As\nthe tests assume that the default branch is called \"main\" we set up the\ncorresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\naccordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5510-fetch.sh        | 53 ----------------------------------\n t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 63 insertions(+), 53 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex dc44da9c79..4f289063ce 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1118,59 +1118,6 @@ test_expect_success 'fetching with auto-gc does not lock up' '\n \t)\n '\n \n-test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n-\ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n-\tlooooooooooooong-tag -> looooooooooooong-tag\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n-\ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n-\textraaa    -> *\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--no-show-forced-updates' '\n-\tmkdir forced-updates &&\n-\t(\n-\t\tcd forced-updates &&\n-\t\tgit init &&\n-\t\ttest_commit 1 &&\n-\t\ttest_commit 2\n-\t) &&\n-\tgit clone forced-updates forced-update-clone &&\n-\tgit clone forced-updates no-forced-update-clone &&\n-\tgit -C forced-updates reset --hard HEAD~1 &&\n-\t(\n-\t\tcd forced-update-clone &&\n-\t\tgit fetch --show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep \"(forced update)\" output\n-\t) &&\n-\t(\n-\t\tcd no-forced-update-clone &&\n-\t\tgit fetch --no-show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep ! \"(forced update)\" output\n-\t)\n-'\n-\n for section in fetch transfer\n do\n \ttest_expect_success \"$section.hideRefs affects connectivity check\" '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nnew file mode 100755\nindex 0000000000..f91b654d38\n--- /dev/null\n+++ b/t/t5574-fetch-output.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='git fetch output format'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'fetch aligned output' '\n+\tgit clone . full-output &&\n+\ttest_commit looooooooooooong-tag &&\n+\t(\n+\t\tcd full-output &&\n+\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain                 -> origin/main\n+\tlooooooooooooong-tag -> looooooooooooong-tag\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'fetch compact output' '\n+\tgit clone . compact &&\n+\ttest_commit extraaa &&\n+\t(\n+\t\tcd compact &&\n+\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain       -> origin/*\n+\textraaa    -> *\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-show-forced-updates' '\n+\tmkdir forced-updates &&\n+\t(\n+\t\tcd forced-updates &&\n+\t\tgit init &&\n+\t\ttest_commit 1 &&\n+\t\ttest_commit 2\n+\t) &&\n+\tgit clone forced-updates forced-update-clone &&\n+\tgit clone forced-updates no-forced-update-clone &&\n+\tgit -C forced-updates reset --hard HEAD~1 &&\n+\t(\n+\t\tcd forced-update-clone &&\n+\t\tgit fetch --show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep \"(forced update)\" output\n+\t) &&\n+\t(\n+\t\tcd no-forced-update-clone &&\n+\t\tgit fetch --no-show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep ! \"(forced update)\" output\n+\t)\n+'\n+\n+test_done\n-- \n2.40.1\n\n"},{"id":"476501","messageId":"78479922ac38eeb6a74f721da73b5437f62ae464.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 3/8] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:27Z","receivedAt":"2023-05-03T11:34:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add a testcase that exercises the logic when an invalid output format is\npassed via the `fetch.output` configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5574-fetch-output.sh | 19 +++++++++++++++++++\n 1 file changed, 19 insertions(+)\n\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex f91b654d38..a09750d225 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -7,6 +7,25 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+test_expect_success 'fetch with invalid output format configuration' '\n+\ttest_when_finished \"rm -rf clone\" &&\n+\tgit clone . clone &&\n+\n+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success 'fetch aligned output' '\n \tgit clone . full-output &&\n \ttest_commit looooooooooooong-tag &&\n-- \n2.40.1\n\n"},{"id":"476502","messageId":"46e1266ab0b13ace9b50bd98c00dbabb566c6cd2.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 4/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:31Z","receivedAt":"2023-05-03T11:35:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"`store_updated_refs()` parses the remote reference for two purposes:\n\n    - It gets used as a note when writing FETCH_HEAD.\n\n    - It is passed through to `display_ref_update()` to display\n      updated references in the following format:\n\n      ```\n       * branch               master          -> master\n      ```\n\nIn most cases, the parsed remote reference is the prettified reference\nname and can thus be used for both cases. But if the remote reference is\nHEAD, the parsed remote reference becomes empty. This is intended when\nwe write the FETCH_HEAD, where we skip writing the note in that case.\nBut it is not intended when displaying the updated references and would\ncause us to miss the left-hand side of the displayed reference update:\n\n```\n$ git fetch origin HEAD:foo\nFrom https://github.com/git/git\n * [new ref]                          -> foo\n```\n\nThe HEAD string is clearly missing from the left-hand side of the arrow,\nwhich is further stressed by the point that the following commands show\nthe left-hand side as expected:\n\n```\n$ git fetch origin HEAD\nFrom https://github.com/git/git\n * branch                  HEAD       -> FETCH_HEAD\n\n$ git fetch origin master\nFrom https://github.com/git/git\n * branch                  master     -> FETCH_HEAD\n * branch                  master     -> origin/master\n```\n\nThe logic of how we compute the remote reference name that we ultimately\npass to `display_ref_update()` is not easy to follow. There are three\ndifferent cases here:\n\n    - When the remote reference name is \"HEAD\" we set the remote\n      reference name to the empty string. This is the case that causes\n      the bug to occur, where we would indeed want to print \"HEAD\"\n      instead of the empty string. This is what `prettify_refname()`\n      would return.\n\n    - When the remote reference name has a well-known prefix then we\n      strip this prefix. This matches what `prettify_refname()` does.\n\n    - Otherwise, we keep the fully qualified reference name. This also\n      matches what `prettify_refname()` does.\n\nAs the return value of `prettify_refname()` would do the correct thing\nfor us in all three cases, we can fix the bug by passing through the\nfull remote reference name to `display_ref_update()`, which learns to\ncall `prettify_refname()`. At the same time, this also simplifies the\ncode a bit.\n\nNote that this patch also changes formatting of the block that computes\nthe \"kind\" and \"what\" variables. This is done on purpose so that it is\npart of the diff, hopefully making the change easier to comprehend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c         | 37 +++++++++++++++++++------------------\n t/t5574-fetch-output.sh | 29 +++++++++++++++++++++++++++++\n 2 files changed, 48 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 08d7fc7233..6aecf549e8 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t}\n \n \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\tremote = prettify_refname(remote);\n+\tlocal = prettify_refname(local);\n \n \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n \tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n+\t\tprint_remote_to_local(display_state, remote, local);\n \telse\n-\t\tprint_compact(display_state, remote, prettify_refname(local));\n+\t\tprint_compact(display_state, remote, local);\n \tif (error)\n \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n \tstrbuf_addch(&display_state->buf, '\\n');\n@@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n-\t\t\t    const char *remote, const struct ref *remote_ref,\n+\t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width)\n {\n \tstruct commit *current = NULL, *updated;\n@@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn r;\n \t}\n \n@@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = \"\";\n-\t\t\t}\n-\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n \t\t\t\tkind = \"branch\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n \t\t\t\tkind = \"tag\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n \t\t\t\tkind = \"remote-tracking branch\";\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = rm->name;\n \t\t\t}\n@@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t\t  display_state->url_len);\n \n \t\t\tif (ref) {\n-\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n+\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n \t\t\t\t\t\t       rm, summary_width);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n@@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t */\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n-\t\t\t\t\t\t   *what ? what : \"HEAD\",\n+\t\t\t\t\t\t   rm->name,\n \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n \t\t\t}\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex a09750d225..6e0f7e0046 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -56,6 +56,35 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\n+\tgit -C head fetch --dry-run origin HEAD >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * branch            HEAD       -> FETCH_HEAD\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch --dry-run origin HEAD:foo >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         HEAD       -> foo\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD:foo >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476503","messageId":"acc0f7f520972b6935bfbc2d7aaf45501155f4ef.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 5/8] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:36Z","receivedAt":"2023-05-03T11:35:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We currently have two different display formats in git-fetch(1) with the\n\"full\" and \"compact\" formats. This is tracked with a boolean value that\nsimply denotes whether the display format is supposed to be compacted\nor not. This works reasonably well while there are only two formats, but\nwe're about to introduce another format that will make this a bit more\nawkward to use.\n\nIntroduce a `enum display_format` that is more readily extensible.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 101 ++++++++++++++++++++++++++++++------------------\n 1 file changed, 64 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 6aecf549e8..9e7e45344d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -48,11 +48,17 @@ enum {\n \tTAGS_SET = 2\n };\n \n+enum display_format {\n+\tDISPLAY_FORMAT_UNKNOWN = 0,\n+\tDISPLAY_FORMAT_FULL,\n+\tDISPLAY_FORMAT_COMPACT,\n+};\n+\n struct display_state {\n \tstruct strbuf buf;\n \n \tint refcol_width;\n-\tint compact_format;\n+\tenum display_format format;\n \n \tchar *url;\n \tint url_len, shown_url;\n@@ -784,7 +790,6 @@ static int refcol_width(const struct ref *ref, int compact_format)\n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n \t\t\t       const char *raw_url)\n {\n-\tstruct ref *rm;\n \tconst char *format = \"full\";\n \tint i;\n \n@@ -809,31 +814,42 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \tgit_config_get_string_tmp(\"fetch.output\", &format);\n \tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->compact_format = 0;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n \telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->compact_format = 1;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = 10;\n-\tfor (rm = ref_map; rm; rm = rm->next) {\n-\t\tint width;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tstruct ref *rm;\n \n-\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n-\t\t    !rm->peer_ref ||\n-\t\t    !strcmp(rm->name, \"HEAD\"))\n-\t\t\tcontinue;\n+\t\tdisplay_state->refcol_width = 10;\n+\t\tfor (rm = ref_map; rm; rm = rm->next) {\n+\t\t\tint width;\n \n-\t\twidth = refcol_width(rm, display_state->compact_format);\n+\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n+\t\t\t    !rm->peer_ref ||\n+\t\t\t    !strcmp(rm->name, \"HEAD\"))\n+\t\t\t\tcontinue;\n \n-\t\t/*\n-\t\t * Not precise calculation for compact mode because '*' can\n-\t\t * appear on the left hand side of '->' and shrink the column\n-\t\t * back.\n-\t\t */\n-\t\tif (display_state->refcol_width < width)\n-\t\t\tdisplay_state->refcol_width = width;\n+\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n+\n+\t\t\t/*\n+\t\t\t * Not precise calculation for compact mode because '*' can\n+\t\t\t * appear on the left hand side of '->' and shrink the column\n+\t\t\t * back.\n+\t\t\t */\n+\t\t\tif (display_state->refcol_width < width)\n+\t\t\t\tdisplay_state->refcol_width = width;\n+\t\t}\n+\n+\t\tbreak;\n+\t}\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n }\n \n@@ -904,30 +920,41 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *remote, const char *local,\n \t\t\t       int summary_width)\n {\n-\tint width;\n-\n \tif (verbosity < 0)\n \t\treturn;\n \n \tstrbuf_reset(&display_state->buf);\n \n-\tif (!display_state->shown_url) {\n-\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n-\t\t\t    display_state->url_len, display_state->url);\n-\t\tdisplay_state->shown_url = 1;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tint width;\n+\n+\t\tif (!display_state->shown_url) {\n+\t\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n+\t\t\t\t    display_state->url_len, display_state->url);\n+\t\t\tdisplay_state->shown_url = 1;\n+\t\t}\n+\n+\t\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\t\tremote = prettify_refname(remote);\n+\t\tlocal = prettify_refname(local);\n+\n+\t\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n+\n+\t\tif (display_state->format != DISPLAY_FORMAT_COMPACT)\n+\t\t\tprint_remote_to_local(display_state, remote, local);\n+\t\telse\n+\t\t\tprint_compact(display_state, remote, local);\n+\n+\t\tif (error)\n+\t\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\n+\t\tbreak;\n \t}\n-\n-\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n-\tremote = prettify_refname(remote);\n-\tlocal = prettify_refname(local);\n-\n-\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n-\tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, local);\n-\telse\n-\t\tprint_compact(display_state, remote, local);\n-\tif (error)\n-\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n \tfputs(display_state->buf.buf, stderr);\n-- \n2.40.1\n\n"},{"id":"476504","messageId":"cd23440128ea3057c2f5110d230417abc59bdc3c.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 6/8] fetch: move display format parsing into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:40Z","receivedAt":"2023-05-03T11:35:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Parsing the display format happens inside of `display_state_init()`. As\nwe only need to check for a simple config entry, this is a natural\nlocation to put this code as it means that display-state logic is neatly\ncontained in a single location.\n\nWe're about to introduce a output format though that is intended to be\nparseable by machines, for example inside of a script. In that case it\nbecomes a bit awkward of an interface if you have to call git-fetch(1)\nwith the `fetch.output` config key set. We're thus going to introduce a\nnew `--output-format` switch for git-fetch(1) so that the output format\ncan be configured more directly.\n\nThis means we'll have to hook parsing of the display format into the\ncommand line options parser. Having the code to determine the actual\noutput format scattered across two different sites is hard to reason\nabout though.\n\nRefactor the code such that callers are expected to pass the display\nformat that is to be used into `display_state_init()`. This allows us to\nlift up the code into the main function, where we can then hook it into\ncommand line options parser in a follow-up commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 41 ++++++++++++++++++++++++-----------------\n 1 file changed, 24 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9e7e45344d..e15d43dc1e 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -788,14 +788,13 @@ static int refcol_width(const struct ref *ref, int compact_format)\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n-\t\t\t       const char *raw_url)\n+\t\t\t       const char *raw_url, enum display_format format)\n {\n-\tconst char *format = \"full\";\n \tint i;\n \n \tmemset(display_state, 0, sizeof(*display_state));\n-\n \tstrbuf_init(&display_state->buf, 0);\n+\tdisplay_state->format = format;\n \n \tif (raw_url)\n \t\tdisplay_state->url = transport_anonymize_url(raw_url);\n@@ -812,15 +811,6 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \tif (verbosity < 0)\n \t\treturn;\n \n-\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n-\telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n-\telse\n-\t\tdie(_(\"invalid value for '%s': '%s'\"),\n-\t\t    \"fetch.output\", format);\n-\n \tswitch (display_state->format) {\n \tcase DISPLAY_FORMAT_FULL:\n \tcase DISPLAY_FORMAT_COMPACT: {\n@@ -1614,7 +1604,8 @@ static int backfill_tags(struct display_state *display_state,\n }\n \n static int do_fetch(struct transport *transport,\n-\t\t    struct refspec *rs)\n+\t\t    struct refspec *rs,\n+\t\t    enum display_format display_format)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1700,7 +1691,7 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n-\tdisplay_state_init(&display_state, ref_map, transport->url);\n+\tdisplay_state_init(&display_state, ref_map, transport->url, display_format);\n \n \tif (atomic_fetch) {\n \t\ttransaction = ref_transaction_begin(&err);\n@@ -2078,7 +2069,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n-\t\t     int prune_tags_ok, int use_stdin_refspecs)\n+\t\t     int prune_tags_ok, int use_stdin_refspecs,\n+\t\t     enum display_format display_format)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2145,7 +2137,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs);\n+\texit_code = do_fetch(gtransport, &rs, display_format);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2157,6 +2149,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tconst char *bundle_uri;\n+\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n \tint result = 0;\n@@ -2183,6 +2176,19 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n \n+\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n+\t\tconst char *format = \"full\";\n+\n+\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n+\t\tif (!strcasecmp(format, \"full\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n+\t\telse if (!strcasecmp(format, \"compact\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t    \"fetch.output\", format);\n+\t}\n+\n \tif (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)\n \t\trecurse_submodules = recurse_submodules_cli;\n \n@@ -2311,7 +2317,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t} else if (remote) {\n \t\tif (filter_options.choice || has_promisor_remote())\n \t\t\tfetch_one_setup_partial(remote);\n-\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n+\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n+\t\t\t\t   display_format);\n \t} else {\n \t\tint max_children = max_jobs;\n \n-- \n2.40.1\n\n"},{"id":"476505","messageId":"edbc31013f015bbb316c01254d79bf1e9798a34a.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 7/8] fetch: move option related variables into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:44Z","receivedAt":"2023-05-03T11:35:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The options of git-fetch(1) which we pass to `parse_options()` are\ndeclared globally in `builtin/fetch.c`. This means we're forced to use\nglobal variables for all the options, which is more likely to cause\nconfusion than explicitly passing state around.\n\nRefactor the code to move the options into `cmd_fetch()`. Move variables\nthat were previously forced to be declared globally and which are only\nused by `cmd_fetch()` into function-local scope.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 197 ++++++++++++++++++++++++------------------------\n 1 file changed, 100 insertions(+), 97 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e15d43dc1e..820ec7285c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -75,13 +75,12 @@ static int fetch_prune_tags_config = -1; /* unspecified */\n static int prune_tags = -1; /* unspecified */\n #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */\n \n-static int all, append, dry_run, force, keep, multiple, update_head_ok;\n+static int append, dry_run, force, keep, update_head_ok;\n static int write_fetch_head = 1;\n static int verbosity, deepen_relative, set_upstream, refetch;\n static int progress = -1;\n-static int enable_auto_gc = 1;\n-static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;\n-static int max_jobs = -1, submodule_fetch_jobs_config = -1;\n+static int tags = TAGS_DEFAULT, update_shallow, deepen;\n+static int submodule_fetch_jobs_config = -1;\n static int fetch_parallel_config = 1;\n static int atomic_fetch;\n static enum transport_family family;\n@@ -92,17 +91,11 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n-static const char *submodule_prefix = \"\";\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n-static int fetch_write_commit_graph = -1;\n-static int stdin_refspecs = 0;\n-static int negotiate_only;\n \n static int git_fetch_config(const char *k, const char *v, void *cb)\n {\n@@ -160,92 +153,6 @@ static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-static struct option builtin_fetch_options[] = {\n-\tOPT__VERBOSITY(&verbosity),\n-\tOPT_BOOL(0, \"all\", &all,\n-\t\t N_(\"fetch from all remotes\")),\n-\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n-\t\t N_(\"set upstream for git pull/fetch\")),\n-\tOPT_BOOL('a', \"append\", &append,\n-\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n-\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n-\t\t N_(\"use atomic transaction to update references\")),\n-\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to upload pack on remote end\")),\n-\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n-\tOPT_BOOL('m', \"multiple\", &multiple,\n-\t\t N_(\"fetch from multiple remotes\")),\n-\tOPT_SET_INT('t', \"tags\", &tags,\n-\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n-\tOPT_SET_INT('n', NULL, &tags,\n-\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules fetched in parallel\")),\n-\tOPT_BOOL(0, \"prefetch\", &prefetch,\n-\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n-\tOPT_BOOL('p', \"prune\", &prune,\n-\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n-\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n-\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n-\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n-\t\t    N_(\"control recursive fetching of submodules\"),\n-\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"dry-run\", &dry_run,\n-\t\t N_(\"dry run\")),\n-\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n-\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n-\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n-\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n-\t\t    N_(\"allow updating of HEAD ref\")),\n-\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n-\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n-\t\t   N_(\"deepen history of shallow clone\")),\n-\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n-\t\t   N_(\"deepen history of shallow repository based on time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n-\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n-\t\t    N_(\"deepen history of shallow clone\")),\n-\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n-\t\t      N_(\"convert to a complete repository\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n-\t\t      N_(\"re-fetch without negotiating common commits\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n-\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n-\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n-\t\t   N_(\"default for recursive fetching of submodules \"\n-\t\t      \"(lower priority than config files)\"),\n-\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n-\t\t N_(\"accept refs that update .git/shallow\")),\n-\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n-\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n-\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV4),\n-\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV6),\n-\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n-\t\t\tN_(\"report that we have only objects reachable from this object\")),\n-\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n-\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n-\t\t N_(\"check for forced-updates on all updated branches\")),\n-\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n-\t\t N_(\"write the commit-graph after fetching\")),\n-\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n-\t\t N_(\"accept refspecs from stdin\")),\n-\tOPT_END()\n-};\n-\n static void unlock_pack(unsigned int flags)\n {\n \tif (gtransport)\n@@ -2147,13 +2054,109 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n-\tint i;\n \tconst char *bundle_uri;\n+\tconst char *submodule_prefix = \"\";\n \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n+\tint all = 0, multiple = 0;\n \tint result = 0;\n \tint prune_tags_ok = 1;\n+\tint enable_auto_gc = 1;\n+\tint unshallow = 0;\n+\tint max_jobs = -1;\n+\tint recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n+\tint recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n+\tint fetch_write_commit_graph = -1;\n+\tint stdin_refspecs = 0;\n+\tint negotiate_only = 0;\n+\tint i;\n+\n+\tstruct option builtin_fetch_options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_BOOL(0, \"all\", &all,\n+\t\t\t N_(\"fetch from all remotes\")),\n+\t\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n+\t\t\t N_(\"set upstream for git pull/fetch\")),\n+\t\tOPT_BOOL('a', \"append\", &append,\n+\t\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n+\t\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n+\t\t\t N_(\"use atomic transaction to update references\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to upload pack on remote end\")),\n+\t\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n+\t\tOPT_BOOL('m', \"multiple\", &multiple,\n+\t\t\t N_(\"fetch from multiple remotes\")),\n+\t\tOPT_SET_INT('t', \"tags\", &tags,\n+\t\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n+\t\tOPT_SET_INT('n', NULL, &tags,\n+\t\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules fetched in parallel\")),\n+\t\tOPT_BOOL(0, \"prefetch\", &prefetch,\n+\t\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n+\t\tOPT_BOOL('p', \"prune\", &prune,\n+\t\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n+\t\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n+\t\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n+\t\t\t    N_(\"control recursive fetching of submodules\"),\n+\t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n+\t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n+\t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n+\t\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n+\t\t\t    N_(\"allow updating of HEAD ref\")),\n+\t\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n+\t\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n+\t\t\t   N_(\"deepen history of shallow clone\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n+\t\t\t   N_(\"deepen history of shallow repository based on time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n+\t\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n+\t\t\t    N_(\"deepen history of shallow clone\")),\n+\t\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n+\t\t\t      N_(\"convert to a complete repository\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n+\t\t\t      N_(\"re-fetch without negotiating common commits\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n+\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n+\t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n+\t\t\t   N_(\"default for recursive fetching of submodules \"\n+\t\t\t      \"(lower priority than config files)\"),\n+\t\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n+\t\t\t N_(\"accept refs that update .git/shallow\")),\n+\t\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n+\t\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV4),\n+\t\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV6),\n+\t\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n+\t\t\t\tN_(\"report that we have only objects reachable from this object\")),\n+\t\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n+\t\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n+\t\t\t N_(\"check for forced-updates on all updated branches\")),\n+\t\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n+\t\t\t N_(\"write the commit-graph after fetching\")),\n+\t\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n+\t\t\t N_(\"accept refspecs from stdin\")),\n+\t\tOPT_END()\n+\t};\n \n \tpacket_trace_identity(\"fetch\");\n \n-- \n2.40.1\n\n"},{"id":"476506","messageId":"e132d9494ee7d8c4eb3a7a82b6420743c8c7a9b0.1683113177.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"[PATCH v3 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:34:48Z","receivedAt":"2023-05-03T11:35:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The output of git-fetch(1) is obviously designed for consumption by\nusers, only: we neatly columnize data, we abbreviate reference names, we\nprint neat arrows and we don't provide information about actual object\nIDs that have changed. This makes the output format basically unusable\nin the context of scripted invocations of git-fetch(1) that want to\nlearn about the exact changes that the command performs.\n\nIntroduce a new machine-parseable \"porcelain\" output format that is\nsupposed to fix this shortcoming. This output format is intended to\nprovide information about every reference that is about to be updated,\nthe old object ID that the reference has been pointing to and the new\nobject ID it will be updated to. Furthermore, the output format provides\nthe same flags as the human-readable format to indicate basic conditions\nfor each reference update like whether it was a fast-forward update, a\nbranch deletion, a rejected update or others.\n\nThe output format is quite simple:\n\n```\n<flag> <old-object-id> <new-object-id> <local-reference>\\n\n```\n\nWe assume two conditions which are generally true:\n\n    - The old and new object IDs have fixed known widths and cannot\n      contain spaces.\n\n    - References cannot contain newlines.\n\nWith these assumptions, the output format becomes unambiguously\nparseable. Furthermore, given that this output is designed to be\nconsumed by scripts, the machine-readable data is printed to stdout\ninstead of stderr like the human-readable output is. This is mostly done\nso that other data printed to stderr, like error messages or progress\nmeters, don't interfere with the parseable data.\n\nA notable ommission here is that the output format does not include the\nremote from which a reference was fetched, which might be important\ninformation especially in the context of multi-remote fetches. But as\nsuch a format would require us to print the remote for every single\nreference update due to parallelizable fetches it feels wasteful for the\nmost likely usecase, which is when fetching from a single remote.\n\nIn a similar spirit, a second restriction is that this cannot be used\nwith `--recurse-submodules`. This is because any reference updates would\nbe ambiguous without also printing the repository in which the update\nhappens.\n\nConsidering that both multi-remote and submodule fetches are rather\nniche and likely not going to be useful for the majority of usecases\nthese omissions feel acceptable. If usecases for either of these come up\nin the future though it is easy enough to add a new \"porcelain-v2\"\nformat that adds this information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/fetch-options.txt |   6 ++\n Documentation/git-fetch.txt     |   9 +++\n builtin/fetch.c                 |  91 +++++++++++++++++++-----\n t/t5574-fetch-output.sh         | 120 ++++++++++++++++++++++++++++++++\n 4 files changed, 208 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 622bd84768..02a5ae21ef 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -78,6 +78,12 @@ linkgit:git-config[1].\n --dry-run::\n \tShow what would be done, without making any changes.\n \n+--porcelain::\n+\tPrint the output to standard output in an easy-to-parse format for\n+\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n++\n+This is incompatible with `--recurse-submodules=[yes|on-demand]`.\n+\n ifndef::git-pull[]\n --[no-]write-fetch-head::\n \tWrite the list of remote refs fetched in the `FETCH_HEAD`\ndiff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\nindex fba66f1460..f123139c58 100644\n--- a/Documentation/git-fetch.txt\n+++ b/Documentation/git-fetch.txt\n@@ -204,6 +204,15 @@ representing the status of a single ref. Each line is of the form:\n  <flag> <summary> <from> -> <to> [<reason>]\n -------------------------------\n \n+When using `--porcelain`, the output format is intended to be\n+machine-parseable. In contrast to the human-readable output formats it\n+thus prints to standard output instead of standard error. Each line is\n+of the form:\n+\n+-------------------------------\n+<flag> <old-object-id> <new-object-id> <local-reference>\n+-------------------------------\n+\n The status of up-to-date refs is shown only if the --verbose option is\n used.\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 820ec7285c..187c4d373c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,7 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_PORCELAIN,\n };\n \n struct display_state {\n@@ -745,6 +746,9 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\t/* We don't need to precompute anything here. */\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n@@ -815,8 +819,12 @@ static void print_compact(struct display_state *display_state,\n static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *summary, const char *error,\n \t\t\t       const char *remote, const char *local,\n+\t\t\t       const struct object_id *old_oid,\n+\t\t\t       const struct object_id *new_oid,\n \t\t\t       int summary_width)\n {\n+\tFILE *f = stderr;\n+\n \tif (verbosity < 0)\n \t\treturn;\n \n@@ -849,12 +857,17 @@ static void display_ref_update(struct display_state *display_state, char code,\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\tstrbuf_addf(&display_state->buf, \"%c %s %s %s\", code,\n+\t\t\t    oid_to_hex(old_oid), oid_to_hex(new_oid), local);\n+\t\tf = stdout;\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n-\tfputs(display_state->buf.buf, stderr);\n+\tfputs(display_state->buf.buf, f);\n }\n \n static int update_local_ref(struct ref *ref,\n@@ -872,7 +885,8 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -885,7 +899,8 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -896,12 +911,14 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -934,7 +951,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn r;\n \t}\n \n@@ -956,7 +974,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -968,12 +987,14 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1214,7 +1235,9 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n \t\t\t\t\t\t   rm->name,\n-\t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n+\t\t\t\t\t\t   \"FETCH_HEAD\",\n+\t\t\t\t\t\t   &rm->new_oid, &rm->old_oid,\n+\t\t\t\t\t\t   summary_width);\n \t\t\t}\n \t\t}\n \t}\n@@ -1354,6 +1377,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\tdisplay_ref_update(display_state, '-', _(\"[deleted]\"), NULL,\n \t\t\t\t\t   _(\"(none)\"), ref->name,\n+\t\t\t\t\t   &ref->new_oid, &ref->old_oid,\n \t\t\t\t\t   summary_width);\n \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n \t\t}\n@@ -1786,7 +1810,8 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n \treturn 1;\n }\n \n-static void add_options_to_argv(struct strvec *argv)\n+static void add_options_to_argv(struct strvec *argv,\n+\t\t\t\tenum display_format format)\n {\n \tif (dry_run)\n \t\tstrvec_push(argv, \"--dry-run\");\n@@ -1822,6 +1847,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--ipv6\");\n \tif (!write_fetch_head)\n \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n+\tif (format == DISPLAY_FORMAT_PORCELAIN)\n+\t\tstrvec_pushf(argv, \"--porcelain\");\n }\n \n /* Fetch multiple remotes in parallel */\n@@ -1830,6 +1857,7 @@ struct parallel_fetch_state {\n \tconst char **argv;\n \tstruct string_list *remotes;\n \tint next, result;\n+\tenum display_format format;\n };\n \n static int fetch_next_remote(struct child_process *cp,\n@@ -1849,7 +1877,7 @@ static int fetch_next_remote(struct child_process *cp,\n \tstrvec_push(&cp->args, remote);\n \tcp->git_cmd = 1;\n \n-\tif (verbosity >= 0)\n+\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n \t\tprintf(_(\"Fetching %s\\n\"), remote);\n \n \treturn 1;\n@@ -1881,7 +1909,8 @@ static int fetch_finished(int result, struct strbuf *out,\n \treturn 0;\n }\n \n-static int fetch_multiple(struct string_list *list, int max_children)\n+static int fetch_multiple(struct string_list *list, int max_children,\n+\t\t\t  enum display_format format)\n {\n \tint i, result = 0;\n \tstruct strvec argv = STRVEC_INIT;\n@@ -1894,10 +1923,10 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \tstrvec_pushl(&argv, \"fetch\", \"--append\", \"--no-auto-gc\",\n \t\t     \"--no-write-commit-graph\", NULL);\n-\tadd_options_to_argv(&argv);\n+\tadd_options_to_argv(&argv, format);\n \n \tif (max_children != 1 && list->nr != 1) {\n-\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n+\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0, format };\n \t\tconst struct run_process_parallel_opts opts = {\n \t\t\t.tr2_category = \"fetch\",\n \t\t\t.tr2_label = \"parallel/fetch\",\n@@ -1921,7 +1950,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \t\t\tstrvec_pushv(&cmd.args, argv.v);\n \t\t\tstrvec_push(&cmd.args, name);\n-\t\t\tif (verbosity >= 0)\n+\t\t\tif (verbosity >= 0 && format != DISPLAY_FORMAT_PORCELAIN)\n \t\t\t\tprintf(_(\"Fetching %s\\n\"), name);\n \t\t\tcmd.git_cmd = 1;\n \t\t\tif (run_command(&cmd)) {\n@@ -2052,6 +2081,13 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \treturn exit_code;\n }\n \n+static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n+{\n+\tenum display_format *format = opt->value;\n+\t*format = DISPLAY_FORMAT_PORCELAIN;\n+\treturn 0;\n+}\n+\n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tconst char *bundle_uri;\n@@ -2104,6 +2140,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n \t\t\t N_(\"dry run\")),\n+\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n+\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n@@ -2222,6 +2260,23 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tfetch_config_from_gitmodules(sfjc, rs);\n \t}\n \n+\tif (display_format == DISPLAY_FORMAT_PORCELAIN) {\n+\t\tswitch (recurse_submodules_cli) {\n+\t\tcase RECURSE_SUBMODULES_OFF:\n+\t\tcase RECURSE_SUBMODULES_DEFAULT:\n+\t\t\t/*\n+\t\t\t * Reference updates in submodules would be ambiguous\n+\t\t\t * in porcelain mode, so we reject this combination.\n+\t\t\t */\n+\t\t\trecurse_submodules = RECURSE_SUBMODULES_OFF;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--porcelain\", \"--recurse-submodules\");\n+\t\t}\n+\t}\n+\n \tif (negotiate_only && !negotiation_tip.nr)\n \t\tdie(_(\"--negotiate-only needs one or more --negotiation-tip=*\"));\n \n@@ -2341,7 +2396,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\tmax_children = fetch_parallel_config;\n \n \t\t/* TODO should this also die if we have a previous partial-clone? */\n-\t\tresult = fetch_multiple(&list, max_children);\n+\t\tresult = fetch_multiple(&list, max_children, display_format);\n \t}\n \n \n@@ -2363,7 +2418,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tif (max_children < 0)\n \t\t\tmax_children = fetch_parallel_config;\n \n-\t\tadd_options_to_argv(&options);\n+\t\tadd_options_to_argv(&options, display_format);\n \t\tresult = fetch_submodules(the_repository,\n \t\t\t\t\t  &options,\n \t\t\t\t\t  submodule_prefix,\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 6e0f7e0046..18949e9323 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -56,6 +56,102 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\t# Set up a bunch of references that we can use to demonstrate different\n+\t# kinds of flag symbols in the output format.\n+\tMAIN_OLD=$(git rev-parse HEAD) &&\n+\tgit branch \"fast-forward\" &&\n+\tgit branch \"deleted-branch\" &&\n+\tgit checkout -b force-updated &&\n+\ttest_commit --no-tag force-update-old &&\n+\tFORCE_UPDATED_OLD=$(git rev-parse HEAD) &&\n+\tgit checkout main &&\n+\n+\t# Clone and pre-seed the repositories. We fetch references into two\n+\t# namespaces so that we can test that rejected and force-updated\n+\t# references are reported properly.\n+\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n+\tgit clone . porcelain &&\n+\tgit -C porcelain fetch origin $refspecs &&\n+\n+\t# Now that we have set up the client repositories we can change our\n+\t# local references.\n+\tgit branch new-branch &&\n+\tgit branch -d deleted-branch &&\n+\tgit checkout fast-forward &&\n+\ttest_commit --no-tag fast-forward-new &&\n+\tFAST_FORWARD_NEW=$(git rev-parse HEAD) &&\n+\tgit checkout force-updated &&\n+\tgit reset --hard HEAD~ &&\n+\ttest_commit --no-tag force-update-new &&\n+\tFORCE_UPDATED_NEW=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n+\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n+\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\tEOF\n+\n+\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n+\t# and non-dry-run fetches produces the same output. Execution of the\n+\t# fetch is expected to fail as we have a rejected reference update.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --dry-run --prune origin $refspecs >actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# And now we perform a non-dry-run fetch.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --prune origin $refspecs >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain with multiple remotes' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tgit clone . porcelain &&\n+\tgit -C porcelain remote add second-remote \"$PWD\" &&\n+\tgit -C porcelain fetch second-remote &&\n+\n+\ttest_commit --no-tag multi-commit &&\n+\told_commit=$(git rev-parse HEAD~) &&\n+\tnew_commit=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t  $old_commit $new_commit refs/remotes/origin/force-updated\n+\t  $old_commit $new_commit refs/remotes/second-remote/force-updated\n+\tEOF\n+\n+\tgit -C porcelain fetch --porcelain --all >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain refuses to work with submodules' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tcat >expect <<-EOF &&\n+\tfatal: options ${SQ}--porcelain${SQ} and ${SQ}--recurse-submodules${SQ} cannot be used together\n+\tEOF\n+\n+\tgit init porcelain &&\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=yes 2>stderr &&\n+\ttest_cmp expect stderr &&\n+\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=on-demand 2>stderr &&\n+\ttest_cmp expect stderr\n+'\n+\n test_expect_success 'fetch output with HEAD' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n@@ -85,6 +181,30 @@ test_expect_success 'fetch output with HEAD' '\n \ttest_cmp expect actual.err\n '\n \n+test_expect_success 'fetch porcelain output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\tCOMMIT_ID=$(git rev-parse HEAD) &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD:foo >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD:foo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476507","messageId":"ZFJHOLCQpyeWEPB7@ncase","threadId":"59619","inReplyTo":"ZFIsw94SBxRVkWmz@ncase","subject":"Re: [PATCH v2 7/8] fetch: introduce new `--output-format` option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-03T11:36:24Z","receivedAt":"2023-05-03T11:37:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, May 03, 2023 at 11:43:31AM +0200, Patrick Steinhardt wrote:\n> On Fri, Apr 28, 2023 at 03:31:08PM -0700, Glen Choo wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > \n> > > @@ -1894,6 +1902,9 @@ static int fetch_multiple(struct string_list *list, int max_children)\n> > >  \t\t     \"--no-write-commit-graph\", NULL);\n> > >  \tadd_options_to_argv(&argv);\n> > >  \n> > > +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n> > > +\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n> > > +\n> > \n> > I think these lines belong inside add_options_to_argv(), since that's\n> > also used to prepare argv for fetch_submodules(), so we'd also get\n> > support for --recurse-submodules. (I wish I had spotted that in v1,\n> > sorry. Thankfully they use the same helper function, so we only have to\n> > do this once.)\n> > \n> > ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n> >   diff --git a/builtin/fetch.c b/builtin/fetch.c\n> >   index 422e29a914..7aa385aed5 100644\n> >   --- a/builtin/fetch.c\n> >   +++ b/builtin/fetch.c\n> >   @@ -1796,8 +1796,11 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n> >     return 1;\n> >   }\n> > \n> >   -static void add_options_to_argv(struct strvec *argv)\n> >   +static void add_options_to_argv(struct strvec *argv,\n> >   +\t\t\t\tenum display_format format)\n> >   {\n> >   /* Maybe this shouldn't be first, idk */\n> >   +\tif (format != DISPLAY_FORMAT_UNKNOWN)\n> >   +\t\tstrvec_pushf(argv, \"--output-format=%s\", display_formats[format]);\n> >     if (dry_run)\n> >       strvec_push(argv, \"--dry-run\");\n> >     if (prune != -1)\n> >   @@ -1908,10 +1911,7 @@ static int fetch_multiple(struct string_list *list, int max_children,\n> >     strvec_pushl(&argv, \"-c\", \"fetch.bundleURI=\",\n> >           \"fetch\", \"--append\", \"--no-auto-gc\",\n> >           \"--no-write-commit-graph\", NULL);\n> >   -\tadd_options_to_argv(&argv);\n> >   -\n> >   -\tif (format != DISPLAY_FORMAT_UNKNOWN)\n> >   -\t\tstrvec_pushf(&argv, \"--output-format=%s\", display_formats[format]);\n> >   +\tadd_options_to_argv(&argv, format);\n> > \n> >     if (max_children != 1 && list->nr != 1) {\n> >       struct parallel_fetch_state state = { argv.v, list, 0, 0 };\n> >   @@ -2403,7 +2403,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n> >       if (max_children < 0)\n> >         max_children = fetch_parallel_config;\n> > \n> >   -\t\tadd_options_to_argv(&options);\n> >   +\t\tadd_options_to_argv(&options, display_format);\n> >       result = fetch_submodules(the_repository,\n> >               &options,\n> >               submodule_prefix,\n> > \n> > ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n> > \n> > I tested the result of that locally with --recurse-submodules, and\n> > it works.\n> \n> Unfortunately it doesn't quite work alright: while the porcelain format\n> does indeed get inherited to the child process correctly, the parallel\n> process API will cause us to group output per submodule-fetch. This has\n> the consequence that stdout will be redirected into stderr, and that\n> then breaks the assumption that all machine-parseable output goes to\n> stdout.\n> \n> My initial reflex is to just outright reject porcelain mode when\n> submodule fetches are enabled. But that would require the caller to\n> always explicitly pass `--recurse-submodules=off`, which isn't exactly\n> great usability-wise.\n> \n> The alternative would be to ungroup the output so that we can continue\n> to print to the correct output streams. That works alright, and I've got\n> a working version that does exactly that. But now we have the issue that\n> the porcelain output is misleading: you cannot tell whether a specific\n> reference update happens in the parent repository or in the submodule as\n> that information is not part of the output.\n> \n> I consider the second option to be much worse than the first option\n> because it can cause scripts do to the wrong thing. So I'll send v3 with\n> the first option, even though it's kind of an awful workaround. I'd be\n> happy to hear any alternative proposals though.\n> \n> Patrick\n\nI've gone with a slightly different variant of the first option that is\ninspired by `--negotiate-only`: instead of refusing to run, we disable\nsubmodule-fetches unless explicitly specified on the command line. In\nthat case we return an error.\n\nPatrick\n"},{"id":"476520","messageId":"xmqq1qjxuzrc.fsf@gitster.g","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"Re: [PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-03T16:48:55Z","receivedAt":"2023-05-03T16:49:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this is the third version of my patch series to introduce a\n> machine-parseable output for git-fetch(1).\n\nWhich base did you pick to build these 8 patches?  It seems that 6/8\nis not happy when the series is attempted to be applied on top of\nmany recent tips of 'master' that I tried (including 667fcf4e15 that\nthe previous iteration was queued on top of in my tree).\n\nThanks.\n"},{"id":"476521","messageId":"xmqqwn1ptkzz.fsf@gitster.g","threadId":"59619","inReplyTo":"xmqq1qjxuzrc.fsf@gitster.g","subject":"Re: [PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-03T16:53:04Z","receivedAt":"2023-05-03T16:53:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> this is the third version of my patch series to introduce a\n>> machine-parseable output for git-fetch(1).\n>\n> Which base did you pick to build these 8 patches?  It seems that 6/8\n> is not happy when the series is attempted to be applied on top of\n> many recent tips of 'master' that I tried (including 667fcf4e15 that\n> the previous iteration was queued on top of in my tree).\n\nOK, I found e9dffbc7 (Merge branch 'ps/fetch-ref-update-reporting',\n2023-04-06).  I can manage from here.\n\nThanks.\n"},{"id":"476590","messageId":"ZFNlWSo3rSp1WzhW@ncase","threadId":"59619","inReplyTo":"xmqqwn1ptkzz.fsf@gitster.g","subject":"Re: [PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-04T07:57:13Z","receivedAt":"2023-05-04T07:57:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, May 03, 2023 at 09:53:04AM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> >> this is the third version of my patch series to introduce a\n> >> machine-parseable output for git-fetch(1).\n> >\n> > Which base did you pick to build these 8 patches?  It seems that 6/8\n> > is not happy when the series is attempted to be applied on top of\n> > many recent tips of 'master' that I tried (including 667fcf4e15 that\n> > the previous iteration was queued on top of in my tree).\n> \n> OK, I found e9dffbc7 (Merge branch 'ps/fetch-ref-update-reporting',\n> 2023-04-06).  I can manage from here.\n> \n> Thanks.\n\nYeah, that's where I based it off from. Sorry for not explicitly\nmentioning this fact.\n\nPatrick\n"},{"id":"476843","messageId":"kl6llehy1lo7.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"4b2b0cfe15ca6b3c9058bc9cb6944ba05d60a228.1683113177.git.ps@pks.im","subject":"Re: [PATCH v3 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-08T22:51:52Z","receivedAt":"2023-05-08T22:51:57Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When running `git fetch --no-recurse-submodules`, the exectation is that\n> we don't fetch any submodules. And while this works for fetches of a\n> single remote, it doesn't when fetching multiple remotes at once. The\n> result is that we do recurse into submodules even though the user has\n> explicitly asked us not to.\n>\n> This is because while we pass on `--recurse-submodules={yes,on-demand}`\n> if specified by the user, we don't pass on `--no-recurse-submodules` to\n> the subprocess spawned to perform the submodule fetch.\n>\n> Fix this by also forwarding this flag as expected.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/fetch.c             |  2 ++\n>  t/t5526-fetch-submodules.sh | 31 +++++++++++++++++++++++++++++++\n>  2 files changed, 33 insertions(+)\n>\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index c310d89878..08d7fc7233 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -1876,6 +1876,8 @@ static void add_options_to_argv(struct strvec *argv)\n>  \t\tstrvec_push(argv, \"--keep\");\n>  \tif (recurse_submodules == RECURSE_SUBMODULES_ON)\n>  \t\tstrvec_push(argv, \"--recurse-submodules\");\n> +\telse if (recurse_submodules == RECURSE_SUBMODULES_OFF)\n> +\t\tstrvec_push(argv, \"--no-recurse-submodules\");\n>  \telse if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)\n>  \t\tstrvec_push(argv, \"--recurse-submodules=on-demand\");\n>  \tif (tags == TAGS_SET)\n\nMakes sense.\n\nI wondered for a bit whether this should have been checking\nrecurse_submodules_cli (the actual CLI flag) back in 386c076a86 (fetch\n--negotiate-only: do not update submodules, 2022-01-18). I think this\ncurrent state is correct, though. After we have told the superproject\nwhat submodule recursion mode to use, we want to continue using that\nmode when recursing through submodules regardless of whether that mode\noriginally came from the CLI or config.\n\n> +test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n> +\ttest_when_finished \"git -C downstream remote remove second\" &&\n> +\n> +\t# We need to add a second remote, otherwise --all falls back to the\n> +\t# normal fetch-one case.\n> +\tgit -C downstream remote add second .. &&\n> +\tgit -C downstream fetch --all &&\n> +\n> +\tadd_submodule_commits &&\n> +\tadd_superproject_commits &&\n> +\told_commit=$(git rev-parse --short HEAD~) &&\n> +\tnew_commit=$(git rev-parse --short HEAD) &&\n> +\n> +\tgit -C downstream fetch --all --no-recurse-submodules >actual.out 2>actual.err &&\n> +\n> +\tcat >expect.out <<-EOF &&\n> +\tFetching origin\n> +\tFetching second\n> +\tEOF\n> +\n> +\tcat >expect.err <<-EOF &&\n> +\tFrom $(test-tool path-utils real_path .)/.\n> +\t   $old_commit..$new_commit  super      -> origin/super\n> +\tFrom ..\n> +\t   $old_commit..$new_commit  super      -> second/super\n> +\tEOF\n> +\n> +\ttest_cmp expect.out actual.out &&\n> +\ttest_cmp expect.err actual.err\n> +'\n\nThe test looks okay, though is there a reason you didn't copy the style\nof the previous test? It is nearly exactly what you want, I think, like\n(untested)\n\n  test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n    test_when_finished \"rm -fr src_clone\" &&\n    git clone --recurse-submodules src src_clone &&\n    (\n      cd src_clone &&\n      git remote add secondary ../src &&\n      git config submodule.recurse true &&\n      git config fetch.parallel 0 &&\n      git fetch --all 2>../fetch-log\n    ) &&\n    grep \"Fetching submodule\" fetch-log >fetch-subs &&\n    test_must_be_empty fetch-subs\n  '\n\nwhich has the handy benefit of not needing the test-tools invocation.\n"},{"id":"476845","messageId":"kl6lild21jbd.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"e132d9494ee7d8c4eb3a7a82b6420743c8c7a9b0.1683113177.git.ps@pks.im","subject":"Re: [PATCH v3 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-08T23:42:46Z","receivedAt":"2023-05-08T23:42:58Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"This version looks great! I only have minor comments.\n\nPatrick Steinhardt <ps@pks.im> writes:\n\n> A notable ommission here is that the output format does not include the\n> remote from which a reference was fetched, which might be important\n> information especially in the context of multi-remote fetches. But as\n> such a format would require us to print the remote for every single\n> reference update due to parallelizable fetches it feels wasteful for the\n> most likely usecase, which is when fetching from a single remote.\n>\n> In a similar spirit, a second restriction is that this cannot be used\n> with `--recurse-submodules`. This is because any reference updates would\n> be ambiguous without also printing the repository in which the update\n> happens.\n>\n> Considering that both multi-remote and submodule fetches are rather\n> niche and likely not going to be useful for the majority of usecases\n> these omissions feel acceptable. If usecases for either of these come up\n> in the future though it is easy enough to add a new \"porcelain-v2\"\n> format that adds this information.\n\nAs a point of clarification, I think these options aren't niche in\nthemselves, but they are more user-facing, so using them _in conjunction\nwith_ --porcelain is probably pretty niche, so I think this is okay for\nnow.\n\n> --- a/Documentation/fetch-options.txt\n> +++ b/Documentation/fetch-options.txt\n> @@ -78,6 +78,12 @@ linkgit:git-config[1].\n>  --dry-run::\n>  \tShow what would be done, without making any changes.\n>  \n> +--porcelain::\n> +\tPrint the output to standard output in an easy-to-parse format for\n> +\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n> ++\n> +This is incompatible with `--recurse-submodules=[yes|on-demand]`.\n> +\n>  ifndef::git-pull[]\n>  --[no-]write-fetch-head::\n>  \tWrite the list of remote refs fetched in the `FETCH_HEAD`\n> diff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\n> index fba66f1460..f123139c58 100644\n> --- a/Documentation/git-fetch.txt\n> +++ b/Documentation/git-fetch.txt\n> @@ -204,6 +204,15 @@ representing the status of a single ref. Each line is of the form:\n>   <flag> <summary> <from> -> <to> [<reason>]\n>  -------------------------------\n>  \n> +When using `--porcelain`, the output format is intended to be\n> +machine-parseable. In contrast to the human-readable output formats it\n> +thus prints to standard output instead of standard error. Each line is\n> +of the form:\n> +\n> +-------------------------------\n> +<flag> <old-object-id> <new-object-id> <local-reference>\n> +-------------------------------\n> +\n>  The status of up-to-date refs is shown only if the --verbose option is\n>  used.\n\nI hadn't commented on the docs before, but they are very clear. Thanks!\n\n> @@ -1830,6 +1857,7 @@ struct parallel_fetch_state {\n>  \tconst char **argv;\n>  \tstruct string_list *remotes;\n>  \tint next, result;\n> +\tenum display_format format;\n>  };\n>  \n>  static int fetch_next_remote(struct child_process *cp,\n> @@ -1849,7 +1877,7 @@ static int fetch_next_remote(struct child_process *cp,\n>  \tstrvec_push(&cp->args, remote);\n>  \tcp->git_cmd = 1;\n>  \n> -\tif (verbosity >= 0)\n> +\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n>  \t\tprintf(_(\"Fetching %s\\n\"), remote);\n>  \n>  \treturn 1;\n\nHere and elsewhere, I wonder if it's clearer to name the variable\n\"porcelain\" and separate it from \"enum display_format\". Then we can\ncheck \"porcelain\" directly instead of using \"format ==\nDISPLAY_FORMAT_PORCELAIN\" as a proxy...\n\n> +static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n> +{\n> +\tenum display_format *format = opt->value;\n> +\t*format = DISPLAY_FORMAT_PORCELAIN;\n> +\treturn 0;\n> +}\n> +\n>  int cmd_fetch(int argc, const char **argv, const char *prefix)\n>  {\n>  \tconst char *bundle_uri;\n> @@ -2104,6 +2140,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n>  \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n>  \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n>  \t\t\t N_(\"dry run\")),\n> +\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n> +\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n>  \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n>  \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n>  \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n\ne.g. since we are reusing the \"display_format\" variable, we need to make\nsure we parse \"--porcelain\" after we read \"fetch.output\". I\ndouble-checked to make sure we were doing the right thing, though it\nwould be nice to not have to worry about those sorts of things. This\nshouldn't hold up the series though.\n\n> +test_expect_success 'fetch porcelain with multiple remotes' '\n> +\ttest_when_finished \"rm -rf porcelain\" &&\n> +\n> +\tgit clone . porcelain &&\n> +\tgit -C porcelain remote add second-remote \"$PWD\" &&\n> +\tgit -C porcelain fetch second-remote &&\n> +\n> +\ttest_commit --no-tag multi-commit &&\n> +\told_commit=$(git rev-parse HEAD~) &&\n> +\tnew_commit=$(git rev-parse HEAD) &&\n> +\n> +\tcat >expect <<-EOF &&\n> +\t  $old_commit $new_commit refs/remotes/origin/force-updated\n> +\t  $old_commit $new_commit refs/remotes/second-remote/force-updated\n> +\tEOF\n\nThe only thing in this test that relies on the previous test is that\nHEAD is pointing to \"force-updated\", and it's hard to tell where HEAD is\nsince the previous test is so long. Could we create a new branch\ninstead?\n"},{"id":"476848","messageId":"kl6lednq1ic5.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"e132d9494ee7d8c4eb3a7a82b6420743c8c7a9b0.1683113177.git.ps@pks.im","subject":"Re: [PATCH v3 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-09T00:03:54Z","receivedAt":"2023-05-09T00:04:00Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +--porcelain::\n> +\tPrint the output to standard output in an easy-to-parse format for\n> +\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n> ++\n> +This is incompatible with `--recurse-submodules=[yes|on-demand]`.\n> +\n\nWe should probably specify that this will override any setting of\n\"fetch.output\" (and perhaps add a test for it). Most readers will\nprobably guess that this is the case, but the extra clarity wouldn't\nhurt.\n"},{"id":"476849","messageId":"kl6lcz3a1i7v.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"cover.1683113177.git.ps@pks.im","subject":"Re: [PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-09T00:06:28Z","receivedAt":"2023-05-09T00:06:32Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> this is the third version of my patch series to introduce a\n> machine-parseable output for git-fetch(1).\n\nThis version looks great! I only had minor comments this time, mainly on\ndocs and tests.\n\nThanks for your patience and receptiveness, and for the fruitful\ndiscussions :)\n"},{"id":"476871","messageId":"ZFo_eMAwq8LEXBhh@ncase","threadId":"59619","inReplyTo":"kl6llehy1lo7.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v3 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T12:41:28Z","receivedAt":"2023-05-09T12:41:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 08, 2023 at 03:51:52PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> > +test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n> > +\ttest_when_finished \"git -C downstream remote remove second\" &&\n> > +\n> > +\t# We need to add a second remote, otherwise --all falls back to the\n> > +\t# normal fetch-one case.\n> > +\tgit -C downstream remote add second .. &&\n> > +\tgit -C downstream fetch --all &&\n> > +\n> > +\tadd_submodule_commits &&\n> > +\tadd_superproject_commits &&\n> > +\told_commit=$(git rev-parse --short HEAD~) &&\n> > +\tnew_commit=$(git rev-parse --short HEAD) &&\n> > +\n> > +\tgit -C downstream fetch --all --no-recurse-submodules >actual.out 2>actual.err &&\n> > +\n> > +\tcat >expect.out <<-EOF &&\n> > +\tFetching origin\n> > +\tFetching second\n> > +\tEOF\n> > +\n> > +\tcat >expect.err <<-EOF &&\n> > +\tFrom $(test-tool path-utils real_path .)/.\n> > +\t   $old_commit..$new_commit  super      -> origin/super\n> > +\tFrom ..\n> > +\t   $old_commit..$new_commit  super      -> second/super\n> > +\tEOF\n> > +\n> > +\ttest_cmp expect.out actual.out &&\n> > +\ttest_cmp expect.err actual.err\n> > +'\n> \n> The test looks okay, though is there a reason you didn't copy the style\n> of the previous test? It is nearly exactly what you want, I think, like\n> (untested)\n> \n>   test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n>     test_when_finished \"rm -fr src_clone\" &&\n>     git clone --recurse-submodules src src_clone &&\n>     (\n>       cd src_clone &&\n>       git remote add secondary ../src &&\n>       git config submodule.recurse true &&\n>       git config fetch.parallel 0 &&\n>       git fetch --all 2>../fetch-log\n>     ) &&\n>     grep \"Fetching submodule\" fetch-log >fetch-subs &&\n>     test_must_be_empty fetch-subs\n>   '\n> \n> which has the handy benefit of not needing the test-tools invocation.\n\nThat is indeed much simpler, thanks.\n\nPatrick\n"},{"id":"476872","messageId":"ZFo_hHiwRU_7yDsg@ncase","threadId":"59619","inReplyTo":"kl6lild21jbd.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v3 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T12:41:40Z","receivedAt":"2023-05-09T12:41:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 08, 2023 at 04:42:46PM -0700, Glen Choo wrote:\n> This version looks great! I only have minor comments.\n> \n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > A notable ommission here is that the output format does not include the\n> > remote from which a reference was fetched, which might be important\n> > information especially in the context of multi-remote fetches. But as\n> > such a format would require us to print the remote for every single\n> > reference update due to parallelizable fetches it feels wasteful for the\n> > most likely usecase, which is when fetching from a single remote.\n> >\n> > In a similar spirit, a second restriction is that this cannot be used\n> > with `--recurse-submodules`. This is because any reference updates would\n> > be ambiguous without also printing the repository in which the update\n> > happens.\n> >\n> > Considering that both multi-remote and submodule fetches are rather\n> > niche and likely not going to be useful for the majority of usecases\n> > these omissions feel acceptable. If usecases for either of these come up\n> > in the future though it is easy enough to add a new \"porcelain-v2\"\n> > format that adds this information.\n> \n> As a point of clarification, I think these options aren't niche in\n> themselves, but they are more user-facing, so using them _in conjunction\n> with_ --porcelain is probably pretty niche, so I think this is okay for\n> now.\n\nYeah, that's what I indeed intended to say. Will clarify.\n\n[snip]\n> > @@ -1830,6 +1857,7 @@ struct parallel_fetch_state {\n> >  \tconst char **argv;\n> >  \tstruct string_list *remotes;\n> >  \tint next, result;\n> > +\tenum display_format format;\n> >  };\n> >  \n> >  static int fetch_next_remote(struct child_process *cp,\n> > @@ -1849,7 +1877,7 @@ static int fetch_next_remote(struct child_process *cp,\n> >  \tstrvec_push(&cp->args, remote);\n> >  \tcp->git_cmd = 1;\n> >  \n> > -\tif (verbosity >= 0)\n> > +\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n> >  \t\tprintf(_(\"Fetching %s\\n\"), remote);\n> >  \n> >  \treturn 1;\n> \n> Here and elsewhere, I wonder if it's clearer to name the variable\n> \"porcelain\" and separate it from \"enum display_format\". Then we can\n> check \"porcelain\" directly instead of using \"format ==\n> DISPLAY_FORMAT_PORCELAIN\" as a proxy...\n\nFor now I'd like to keep this as-is: it's easier to keep track of this\nwhen there is only a single variable that keeps track of the output\nformat. But if we were to add additional porcelain formats in the future\nI agree that it would be nice to refactor the code as you propose.\n\nI'd rather keep the simpler version for now though where we only have a\nsingle state to worry about.\n\n> > +static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n> > +{\n> > +\tenum display_format *format = opt->value;\n> > +\t*format = DISPLAY_FORMAT_PORCELAIN;\n> > +\treturn 0;\n> > +}\n> > +\n> >  int cmd_fetch(int argc, const char **argv, const char *prefix)\n> >  {\n> >  \tconst char *bundle_uri;\n> > @@ -2104,6 +2140,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n> >  \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n> >  \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n> >  \t\t\t N_(\"dry run\")),\n> > +\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n> > +\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n> >  \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n> >  \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n> >  \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n> \n> e.g. since we are reusing the \"display_format\" variable, we need to make\n> sure we parse \"--porcelain\" after we read \"fetch.output\". I\n> double-checked to make sure we were doing the right thing, though it\n> would be nice to not have to worry about those sorts of things. This\n> shouldn't hold up the series though.\n\nIn fact it is the other way round: we parse `--porcelain` first and then\nonly read \"fetch.output\" in the case where the `display_format` variable\nis still set to `DISPLAY_FORMAT_UNKNOWN`.\n\nSo in the end there is no fragile order dependence here -- it would work\njust the same regardless of whether we first parse command line options\nor the configuration.\n\n> > +test_expect_success 'fetch porcelain with multiple remotes' '\n> > +\ttest_when_finished \"rm -rf porcelain\" &&\n> > +\n> > +\tgit clone . porcelain &&\n> > +\tgit -C porcelain remote add second-remote \"$PWD\" &&\n> > +\tgit -C porcelain fetch second-remote &&\n> > +\n> > +\ttest_commit --no-tag multi-commit &&\n> > +\told_commit=$(git rev-parse HEAD~) &&\n> > +\tnew_commit=$(git rev-parse HEAD) &&\n> > +\n> > +\tcat >expect <<-EOF &&\n> > +\t  $old_commit $new_commit refs/remotes/origin/force-updated\n> > +\t  $old_commit $new_commit refs/remotes/second-remote/force-updated\n> > +\tEOF\n> \n> The only thing in this test that relies on the previous test is that\n> HEAD is pointing to \"force-updated\", and it's hard to tell where HEAD is\n> since the previous test is so long. Could we create a new branch\n> instead?\n\nMakes sense, done.\n\nPatrick\n"},{"id":"476873","messageId":"ZFo_qpzWkEHVhpFx@ncase","threadId":"59619","inReplyTo":"kl6lcz3a1i7v.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v3 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T12:42:18Z","receivedAt":"2023-05-09T12:43:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 08, 2023 at 05:06:28PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Hi,\n> >\n> > this is the third version of my patch series to introduce a\n> > machine-parseable output for git-fetch(1).\n> \n> This version looks great! I only had minor comments this time, mainly on\n> docs and tests.\n> \n> Thanks for your patience and receptiveness, and for the fruitful\n> discussions :)\n\nThank _you_ for the extensive feedback and constructive criticism.\n\nPatrick\n"},{"id":"476874","messageId":"cover.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH v4 0/8] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:01:58Z","receivedAt":"2023-05-09T13:02:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fourth version of my patch series to introduce a\nmachine-parseable output format for git-fetch(1). It applies on top of\ne9dffbc7f1 (Merge branch 'ps/fetch-ref-update-reporting', 2023-04-06).\n\nThere's only a small set of change compared to v3:\n\n    - Patch 1/8: The test added by this change has been significantly\n      simplified.\n\n    - Patch 8/8: Reworded the commit message to clarify that parallel\n      fetches and `--recurse-submodules` aren't niche on their own, but\n      are likely niche combined with `--porcelain` given that they are\n      user-facing while `--porcelain` is script-facing.\n\n    - Patch 8/8: Clarified that `--porcelain` takes precedence over the\n      `fetch.output` config option in the user-facing documentation.\n      I've also added a test to verify that this is the case.\n\n    - Patch 8/8: Amended one test to not depend on the state that its\n      preceding test cretaes.\n\nThanks to Glen for the feedback.\n\nPatrick\n\nPatrick Steinhardt (8):\n  fetch: fix `--no-recurse-submodules` with multi-remote fetches\n  fetch: split out tests for output format\n  fetch: add a test to exercise invalid output formats\n  fetch: fix missing from-reference when fetching HEAD:foo\n  fetch: introduce `display_format` enum\n  fetch: move display format parsing into main function\n  fetch: move option related variables into main function\n  fetch: introduce machine-parseable \"porcelain\" output format\n\n Documentation/fetch-options.txt |   7 +\n Documentation/git-fetch.txt     |   9 +\n builtin/fetch.c                 | 437 +++++++++++++++++++-------------\n t/t5510-fetch.sh                |  53 ----\n t/t5526-fetch-submodules.sh     |  19 ++\n t/t5574-fetch-output.sh         | 251 ++++++++++++++++++\n 6 files changed, 552 insertions(+), 224 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\nRange-diff against v3:\n1:  4b2b0cfe15 ! 1:  d82b42ed34 fetch: fix `--no-recurse-submodules` with multi-remote fetches\n    @@ t/t5526-fetch-submodules.sh: test_expect_success 'fetch --all with --recurse-sub\n      '\n      \n     +test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n    -+\ttest_when_finished \"git -C downstream remote remove second\" &&\n    ++\ttest_when_finished \"rm -rf src_clone\" &&\n     +\n    -+\t# We need to add a second remote, otherwise --all falls back to the\n    -+\t# normal fetch-one case.\n    -+\tgit -C downstream remote add second .. &&\n    -+\tgit -C downstream fetch --all &&\n    ++\tgit clone --recurse-submodules src src_clone &&\n    ++\t(\n    ++\t\tcd src_clone &&\n    ++\t\tgit remote add secondary ../src &&\n    ++\t\tgit config submodule.recurse true &&\n    ++\t\tgit config fetch.parallel 0 &&\n    ++\t\tgit fetch --all --no-recurse-submodules 2>../actual\n    ++\t) &&\n     +\n    -+\tadd_submodule_commits &&\n    -+\tadd_superproject_commits &&\n    -+\told_commit=$(git rev-parse --short HEAD~) &&\n    -+\tnew_commit=$(git rev-parse --short HEAD) &&\n    -+\n    -+\tgit -C downstream fetch --all --no-recurse-submodules >actual.out 2>actual.err &&\n    -+\n    -+\tcat >expect.out <<-EOF &&\n    -+\tFetching origin\n    -+\tFetching second\n    ++\tcat >expect <<-EOF &&\n    ++\tFrom ../src\n    ++\t * [new branch]      master     -> secondary/master\n     +\tEOF\n    -+\n    -+\tcat >expect.err <<-EOF &&\n    -+\tFrom $(test-tool path-utils real_path .)/.\n    -+\t   $old_commit..$new_commit  super      -> origin/super\n    -+\tFrom ..\n    -+\t   $old_commit..$new_commit  super      -> second/super\n    -+\tEOF\n    -+\n    -+\ttest_cmp expect.out actual.out &&\n    -+\ttest_cmp expect.err actual.err\n    ++\ttest_cmp expect actual\n     +'\n     +\n      test_done\n2:  6ebc7450ba = 2:  33112dc51a fetch: split out tests for output format\n3:  78479922ac = 3:  006ea93afb fetch: add a test to exercise invalid output formats\n4:  46e1266ab0 = 4:  e599ea6d33 fetch: fix missing from-reference when fetching HEAD:foo\n5:  acc0f7f520 = 5:  80ac00b0c4 fetch: introduce `display_format` enum\n6:  cd23440128 = 6:  826b8b7bc0 fetch: move display format parsing into main function\n7:  edbc31013f = 7:  20f2e061d6 fetch: move option related variables into main function\n8:  e132d9494e ! 8:  24ae381950 fetch: introduce machine-parseable \"porcelain\" output format\n    @@ Commit message\n         be ambiguous without also printing the repository in which the update\n         happens.\n     \n    -    Considering that both multi-remote and submodule fetches are rather\n    -    niche and likely not going to be useful for the majority of usecases\n    -    these omissions feel acceptable. If usecases for either of these come up\n    -    in the future though it is easy enough to add a new \"porcelain-v2\"\n    -    format that adds this information.\n    +    Considering that both multi-remote and submodule fetches are user-facing\n    +    features, using them in conjunction with `--porcelain` that is intended\n    +    for scripting purposes is likely not going to be useful in the majority\n    +    of cases. With that in mind these restrictions feel acceptable. If\n    +    usecases for either of these come up in the future though it is easy\n    +    enough to add a new \"porcelain-v2\" format that adds this information.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Documentation/fetch-options.txt: linkgit:git-config[1].\n     +\tPrint the output to standard output in an easy-to-parse format for\n     +\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n     ++\n    -+This is incompatible with `--recurse-submodules=[yes|on-demand]`.\n    ++This is incompatible with `--recurse-submodules=[yes|on-demand]` and takes\n    ++precedence over the `fetch.output` config option.\n     +\n      ifndef::git-pull[]\n      --[no-]write-fetch-head::\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +test_expect_success 'fetch porcelain with multiple remotes' '\n     +\ttest_when_finished \"rm -rf porcelain\" &&\n     +\n    ++\tgit switch --create multiple-remotes &&\n     +\tgit clone . porcelain &&\n     +\tgit -C porcelain remote add second-remote \"$PWD\" &&\n     +\tgit -C porcelain fetch second-remote &&\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +\tnew_commit=$(git rev-parse HEAD) &&\n     +\n     +\tcat >expect <<-EOF &&\n    -+\t  $old_commit $new_commit refs/remotes/origin/force-updated\n    -+\t  $old_commit $new_commit refs/remotes/second-remote/force-updated\n    ++\t  $old_commit $new_commit refs/remotes/origin/multiple-remotes\n    ++\t  $old_commit $new_commit refs/remotes/second-remote/multiple-remotes\n     +\tEOF\n     +\n     +\tgit -C porcelain fetch --porcelain --all >actual 2>stderr &&\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=on-demand 2>stderr &&\n     +\ttest_cmp expect stderr\n     +'\n    ++\n    ++test_expect_success 'fetch porcelain overrides fetch.output config' '\n    ++\ttest_when_finished \"rm -rf porcelain\" &&\n    ++\n    ++\tgit switch --create config-override &&\n    ++\tgit clone . porcelain &&\n    ++\ttest_commit new-commit &&\n    ++\told_commit=$(git rev-parse HEAD~) &&\n    ++\tnew_commit=$(git rev-parse HEAD) &&\n    ++\n    ++\tcat >expect <<-EOF &&\n    ++\t  $old_commit $new_commit refs/remotes/origin/config-override\n    ++\t* $ZERO_OID $new_commit refs/tags/new-commit\n    ++\tEOF\n    ++\n    ++\tgit -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&\n    ++\ttest_must_be_empty stderr &&\n    ++\ttest_cmp expect stdout\n    ++'\n     +\n      test_expect_success 'fetch output with HEAD' '\n      \ttest_when_finished \"rm -rf head\" &&\n-- \n2.40.1\n\n"},{"id":"476875","messageId":"d82b42ed345ac7b482bf5dd96456f131ecb8c746.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:02Z","receivedAt":"2023-05-09T13:02:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When running `git fetch --no-recurse-submodules`, the exectation is that\nwe don't fetch any submodules. And while this works for fetches of a\nsingle remote, it doesn't when fetching multiple remotes at once. The\nresult is that we do recurse into submodules even though the user has\nexplicitly asked us not to.\n\nThis is because while we pass on `--recurse-submodules={yes,on-demand}`\nif specified by the user, we don't pass on `--no-recurse-submodules` to\nthe subprocess spawned to perform the submodule fetch.\n\nFix this by also forwarding this flag as expected.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c             |  2 ++\n t/t5526-fetch-submodules.sh | 19 +++++++++++++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c310d89878..08d7fc7233 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1876,6 +1876,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--keep\");\n \tif (recurse_submodules == RECURSE_SUBMODULES_ON)\n \t\tstrvec_push(argv, \"--recurse-submodules\");\n+\telse if (recurse_submodules == RECURSE_SUBMODULES_OFF)\n+\t\tstrvec_push(argv, \"--no-recurse-submodules\");\n \telse if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)\n \t\tstrvec_push(argv, \"--recurse-submodules=on-demand\");\n \tif (tags == TAGS_SET)\ndiff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\nindex dcdbe26a08..ba69cd583f 100755\n--- a/t/t5526-fetch-submodules.sh\n+++ b/t/t5526-fetch-submodules.sh\n@@ -1180,4 +1180,23 @@ test_expect_success 'fetch --all with --recurse-submodules with multiple' '\n \ttest_line_count = 2 fetch-subs\n '\n \n+test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n+\ttest_when_finished \"rm -rf src_clone\" &&\n+\n+\tgit clone --recurse-submodules src src_clone &&\n+\t(\n+\t\tcd src_clone &&\n+\t\tgit remote add secondary ../src &&\n+\t\tgit config submodule.recurse true &&\n+\t\tgit config fetch.parallel 0 &&\n+\t\tgit fetch --all --no-recurse-submodules 2>../actual\n+\t) &&\n+\n+\tcat >expect <<-EOF &&\n+\tFrom ../src\n+\t * [new branch]      master     -> secondary/master\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.40.1\n\n"},{"id":"476876","messageId":"33112dc51a666f70cab808ec21ab692497b37618.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 2/8] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:06Z","receivedAt":"2023-05-09T13:02:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new porcelain mode for the output of\ngit-fetch(1). As part of that we'll be introducing a set of new tests\nthat only relate to the output of this command.\n\nSplit out tests that exercise the output format of git-fetch(1) so that\nit becomes easier to verify this functionality as a standalone unit. As\nthe tests assume that the default branch is called \"main\" we set up the\ncorresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\naccordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5510-fetch.sh        | 53 ----------------------------------\n t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 63 insertions(+), 53 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex dc44da9c79..4f289063ce 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1118,59 +1118,6 @@ test_expect_success 'fetching with auto-gc does not lock up' '\n \t)\n '\n \n-test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n-\ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n-\tlooooooooooooong-tag -> looooooooooooong-tag\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n-\ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n-\textraaa    -> *\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--no-show-forced-updates' '\n-\tmkdir forced-updates &&\n-\t(\n-\t\tcd forced-updates &&\n-\t\tgit init &&\n-\t\ttest_commit 1 &&\n-\t\ttest_commit 2\n-\t) &&\n-\tgit clone forced-updates forced-update-clone &&\n-\tgit clone forced-updates no-forced-update-clone &&\n-\tgit -C forced-updates reset --hard HEAD~1 &&\n-\t(\n-\t\tcd forced-update-clone &&\n-\t\tgit fetch --show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep \"(forced update)\" output\n-\t) &&\n-\t(\n-\t\tcd no-forced-update-clone &&\n-\t\tgit fetch --no-show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep ! \"(forced update)\" output\n-\t)\n-'\n-\n for section in fetch transfer\n do\n \ttest_expect_success \"$section.hideRefs affects connectivity check\" '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nnew file mode 100755\nindex 0000000000..f91b654d38\n--- /dev/null\n+++ b/t/t5574-fetch-output.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='git fetch output format'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'fetch aligned output' '\n+\tgit clone . full-output &&\n+\ttest_commit looooooooooooong-tag &&\n+\t(\n+\t\tcd full-output &&\n+\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain                 -> origin/main\n+\tlooooooooooooong-tag -> looooooooooooong-tag\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'fetch compact output' '\n+\tgit clone . compact &&\n+\ttest_commit extraaa &&\n+\t(\n+\t\tcd compact &&\n+\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain       -> origin/*\n+\textraaa    -> *\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-show-forced-updates' '\n+\tmkdir forced-updates &&\n+\t(\n+\t\tcd forced-updates &&\n+\t\tgit init &&\n+\t\ttest_commit 1 &&\n+\t\ttest_commit 2\n+\t) &&\n+\tgit clone forced-updates forced-update-clone &&\n+\tgit clone forced-updates no-forced-update-clone &&\n+\tgit -C forced-updates reset --hard HEAD~1 &&\n+\t(\n+\t\tcd forced-update-clone &&\n+\t\tgit fetch --show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep \"(forced update)\" output\n+\t) &&\n+\t(\n+\t\tcd no-forced-update-clone &&\n+\t\tgit fetch --no-show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep ! \"(forced update)\" output\n+\t)\n+'\n+\n+test_done\n-- \n2.40.1\n\n"},{"id":"476877","messageId":"006ea93afbef8742a823a9444d2b71785a9b4ec3.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 3/8] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:11Z","receivedAt":"2023-05-09T13:02:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add a testcase that exercises the logic when an invalid output format is\npassed via the `fetch.output` configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5574-fetch-output.sh | 19 +++++++++++++++++++\n 1 file changed, 19 insertions(+)\n\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex f91b654d38..a09750d225 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -7,6 +7,25 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+test_expect_success 'fetch with invalid output format configuration' '\n+\ttest_when_finished \"rm -rf clone\" &&\n+\tgit clone . clone &&\n+\n+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success 'fetch aligned output' '\n \tgit clone . full-output &&\n \ttest_commit looooooooooooong-tag &&\n-- \n2.40.1\n\n"},{"id":"476878","messageId":"e599ea6d335d16b45fa75b223ea8db93e46c627d.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 4/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:15Z","receivedAt":"2023-05-09T13:02:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"`store_updated_refs()` parses the remote reference for two purposes:\n\n    - It gets used as a note when writing FETCH_HEAD.\n\n    - It is passed through to `display_ref_update()` to display\n      updated references in the following format:\n\n      ```\n       * branch               master          -> master\n      ```\n\nIn most cases, the parsed remote reference is the prettified reference\nname and can thus be used for both cases. But if the remote reference is\nHEAD, the parsed remote reference becomes empty. This is intended when\nwe write the FETCH_HEAD, where we skip writing the note in that case.\nBut it is not intended when displaying the updated references and would\ncause us to miss the left-hand side of the displayed reference update:\n\n```\n$ git fetch origin HEAD:foo\nFrom https://github.com/git/git\n * [new ref]                          -> foo\n```\n\nThe HEAD string is clearly missing from the left-hand side of the arrow,\nwhich is further stressed by the point that the following commands show\nthe left-hand side as expected:\n\n```\n$ git fetch origin HEAD\nFrom https://github.com/git/git\n * branch                  HEAD       -> FETCH_HEAD\n\n$ git fetch origin master\nFrom https://github.com/git/git\n * branch                  master     -> FETCH_HEAD\n * branch                  master     -> origin/master\n```\n\nThe logic of how we compute the remote reference name that we ultimately\npass to `display_ref_update()` is not easy to follow. There are three\ndifferent cases here:\n\n    - When the remote reference name is \"HEAD\" we set the remote\n      reference name to the empty string. This is the case that causes\n      the bug to occur, where we would indeed want to print \"HEAD\"\n      instead of the empty string. This is what `prettify_refname()`\n      would return.\n\n    - When the remote reference name has a well-known prefix then we\n      strip this prefix. This matches what `prettify_refname()` does.\n\n    - Otherwise, we keep the fully qualified reference name. This also\n      matches what `prettify_refname()` does.\n\nAs the return value of `prettify_refname()` would do the correct thing\nfor us in all three cases, we can fix the bug by passing through the\nfull remote reference name to `display_ref_update()`, which learns to\ncall `prettify_refname()`. At the same time, this also simplifies the\ncode a bit.\n\nNote that this patch also changes formatting of the block that computes\nthe \"kind\" and \"what\" variables. This is done on purpose so that it is\npart of the diff, hopefully making the change easier to comprehend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c         | 37 +++++++++++++++++++------------------\n t/t5574-fetch-output.sh | 29 +++++++++++++++++++++++++++++\n 2 files changed, 48 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 08d7fc7233..6aecf549e8 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t}\n \n \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\tremote = prettify_refname(remote);\n+\tlocal = prettify_refname(local);\n \n \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n \tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n+\t\tprint_remote_to_local(display_state, remote, local);\n \telse\n-\t\tprint_compact(display_state, remote, prettify_refname(local));\n+\t\tprint_compact(display_state, remote, local);\n \tif (error)\n \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n \tstrbuf_addch(&display_state->buf, '\\n');\n@@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n-\t\t\t    const char *remote, const struct ref *remote_ref,\n+\t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width)\n {\n \tstruct commit *current = NULL, *updated;\n@@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn r;\n \t}\n \n@@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = \"\";\n-\t\t\t}\n-\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n \t\t\t\tkind = \"branch\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n \t\t\t\tkind = \"tag\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n \t\t\t\tkind = \"remote-tracking branch\";\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = rm->name;\n \t\t\t}\n@@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t\t  display_state->url_len);\n \n \t\t\tif (ref) {\n-\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n+\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n \t\t\t\t\t\t       rm, summary_width);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n@@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t */\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n-\t\t\t\t\t\t   *what ? what : \"HEAD\",\n+\t\t\t\t\t\t   rm->name,\n \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n \t\t\t}\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex a09750d225..6e0f7e0046 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -56,6 +56,35 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\n+\tgit -C head fetch --dry-run origin HEAD >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * branch            HEAD       -> FETCH_HEAD\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch --dry-run origin HEAD:foo >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         HEAD       -> foo\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD:foo >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476879","messageId":"80ac00b0c4b0fde61a3edf48d996e3a60327a54d.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 5/8] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:19Z","receivedAt":"2023-05-09T13:02:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We currently have two different display formats in git-fetch(1) with the\n\"full\" and \"compact\" formats. This is tracked with a boolean value that\nsimply denotes whether the display format is supposed to be compacted\nor not. This works reasonably well while there are only two formats, but\nwe're about to introduce another format that will make this a bit more\nawkward to use.\n\nIntroduce a `enum display_format` that is more readily extensible.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 101 ++++++++++++++++++++++++++++++------------------\n 1 file changed, 64 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 6aecf549e8..9e7e45344d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -48,11 +48,17 @@ enum {\n \tTAGS_SET = 2\n };\n \n+enum display_format {\n+\tDISPLAY_FORMAT_UNKNOWN = 0,\n+\tDISPLAY_FORMAT_FULL,\n+\tDISPLAY_FORMAT_COMPACT,\n+};\n+\n struct display_state {\n \tstruct strbuf buf;\n \n \tint refcol_width;\n-\tint compact_format;\n+\tenum display_format format;\n \n \tchar *url;\n \tint url_len, shown_url;\n@@ -784,7 +790,6 @@ static int refcol_width(const struct ref *ref, int compact_format)\n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n \t\t\t       const char *raw_url)\n {\n-\tstruct ref *rm;\n \tconst char *format = \"full\";\n \tint i;\n \n@@ -809,31 +814,42 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \tgit_config_get_string_tmp(\"fetch.output\", &format);\n \tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->compact_format = 0;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n \telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->compact_format = 1;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = 10;\n-\tfor (rm = ref_map; rm; rm = rm->next) {\n-\t\tint width;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tstruct ref *rm;\n \n-\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n-\t\t    !rm->peer_ref ||\n-\t\t    !strcmp(rm->name, \"HEAD\"))\n-\t\t\tcontinue;\n+\t\tdisplay_state->refcol_width = 10;\n+\t\tfor (rm = ref_map; rm; rm = rm->next) {\n+\t\t\tint width;\n \n-\t\twidth = refcol_width(rm, display_state->compact_format);\n+\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n+\t\t\t    !rm->peer_ref ||\n+\t\t\t    !strcmp(rm->name, \"HEAD\"))\n+\t\t\t\tcontinue;\n \n-\t\t/*\n-\t\t * Not precise calculation for compact mode because '*' can\n-\t\t * appear on the left hand side of '->' and shrink the column\n-\t\t * back.\n-\t\t */\n-\t\tif (display_state->refcol_width < width)\n-\t\t\tdisplay_state->refcol_width = width;\n+\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n+\n+\t\t\t/*\n+\t\t\t * Not precise calculation for compact mode because '*' can\n+\t\t\t * appear on the left hand side of '->' and shrink the column\n+\t\t\t * back.\n+\t\t\t */\n+\t\t\tif (display_state->refcol_width < width)\n+\t\t\t\tdisplay_state->refcol_width = width;\n+\t\t}\n+\n+\t\tbreak;\n+\t}\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n }\n \n@@ -904,30 +920,41 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *remote, const char *local,\n \t\t\t       int summary_width)\n {\n-\tint width;\n-\n \tif (verbosity < 0)\n \t\treturn;\n \n \tstrbuf_reset(&display_state->buf);\n \n-\tif (!display_state->shown_url) {\n-\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n-\t\t\t    display_state->url_len, display_state->url);\n-\t\tdisplay_state->shown_url = 1;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tint width;\n+\n+\t\tif (!display_state->shown_url) {\n+\t\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n+\t\t\t\t    display_state->url_len, display_state->url);\n+\t\t\tdisplay_state->shown_url = 1;\n+\t\t}\n+\n+\t\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\t\tremote = prettify_refname(remote);\n+\t\tlocal = prettify_refname(local);\n+\n+\t\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n+\n+\t\tif (display_state->format != DISPLAY_FORMAT_COMPACT)\n+\t\t\tprint_remote_to_local(display_state, remote, local);\n+\t\telse\n+\t\t\tprint_compact(display_state, remote, local);\n+\n+\t\tif (error)\n+\t\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\n+\t\tbreak;\n \t}\n-\n-\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n-\tremote = prettify_refname(remote);\n-\tlocal = prettify_refname(local);\n-\n-\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n-\tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, local);\n-\telse\n-\t\tprint_compact(display_state, remote, local);\n-\tif (error)\n-\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n \tfputs(display_state->buf.buf, stderr);\n-- \n2.40.1\n\n"},{"id":"476880","messageId":"826b8b7bc0d7d6a76f6fd19d8f4a8460af61e9cf.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 6/8] fetch: move display format parsing into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:24Z","receivedAt":"2023-05-09T13:02:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Parsing the display format happens inside of `display_state_init()`. As\nwe only need to check for a simple config entry, this is a natural\nlocation to put this code as it means that display-state logic is neatly\ncontained in a single location.\n\nWe're about to introduce a output format though that is intended to be\nparseable by machines, for example inside of a script. In that case it\nbecomes a bit awkward of an interface if you have to call git-fetch(1)\nwith the `fetch.output` config key set. We're thus going to introduce a\nnew `--output-format` switch for git-fetch(1) so that the output format\ncan be configured more directly.\n\nThis means we'll have to hook parsing of the display format into the\ncommand line options parser. Having the code to determine the actual\noutput format scattered across two different sites is hard to reason\nabout though.\n\nRefactor the code such that callers are expected to pass the display\nformat that is to be used into `display_state_init()`. This allows us to\nlift up the code into the main function, where we can then hook it into\ncommand line options parser in a follow-up commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 41 ++++++++++++++++++++++++-----------------\n 1 file changed, 24 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9e7e45344d..e15d43dc1e 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -788,14 +788,13 @@ static int refcol_width(const struct ref *ref, int compact_format)\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n-\t\t\t       const char *raw_url)\n+\t\t\t       const char *raw_url, enum display_format format)\n {\n-\tconst char *format = \"full\";\n \tint i;\n \n \tmemset(display_state, 0, sizeof(*display_state));\n-\n \tstrbuf_init(&display_state->buf, 0);\n+\tdisplay_state->format = format;\n \n \tif (raw_url)\n \t\tdisplay_state->url = transport_anonymize_url(raw_url);\n@@ -812,15 +811,6 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \tif (verbosity < 0)\n \t\treturn;\n \n-\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n-\telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n-\telse\n-\t\tdie(_(\"invalid value for '%s': '%s'\"),\n-\t\t    \"fetch.output\", format);\n-\n \tswitch (display_state->format) {\n \tcase DISPLAY_FORMAT_FULL:\n \tcase DISPLAY_FORMAT_COMPACT: {\n@@ -1614,7 +1604,8 @@ static int backfill_tags(struct display_state *display_state,\n }\n \n static int do_fetch(struct transport *transport,\n-\t\t    struct refspec *rs)\n+\t\t    struct refspec *rs,\n+\t\t    enum display_format display_format)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1700,7 +1691,7 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n-\tdisplay_state_init(&display_state, ref_map, transport->url);\n+\tdisplay_state_init(&display_state, ref_map, transport->url, display_format);\n \n \tif (atomic_fetch) {\n \t\ttransaction = ref_transaction_begin(&err);\n@@ -2078,7 +2069,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n-\t\t     int prune_tags_ok, int use_stdin_refspecs)\n+\t\t     int prune_tags_ok, int use_stdin_refspecs,\n+\t\t     enum display_format display_format)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2145,7 +2137,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs);\n+\texit_code = do_fetch(gtransport, &rs, display_format);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2157,6 +2149,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tconst char *bundle_uri;\n+\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n \tint result = 0;\n@@ -2183,6 +2176,19 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix,\n \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n \n+\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n+\t\tconst char *format = \"full\";\n+\n+\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n+\t\tif (!strcasecmp(format, \"full\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n+\t\telse if (!strcasecmp(format, \"compact\"))\n+\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t    \"fetch.output\", format);\n+\t}\n+\n \tif (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)\n \t\trecurse_submodules = recurse_submodules_cli;\n \n@@ -2311,7 +2317,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t} else if (remote) {\n \t\tif (filter_options.choice || has_promisor_remote())\n \t\t\tfetch_one_setup_partial(remote);\n-\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n+\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n+\t\t\t\t   display_format);\n \t} else {\n \t\tint max_children = max_jobs;\n \n-- \n2.40.1\n\n"},{"id":"476881","messageId":"20f2e061d602ab5d47f90afdeb406c21fdec1806.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 7/8] fetch: move option related variables into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:28Z","receivedAt":"2023-05-09T13:02:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The options of git-fetch(1) which we pass to `parse_options()` are\ndeclared globally in `builtin/fetch.c`. This means we're forced to use\nglobal variables for all the options, which is more likely to cause\nconfusion than explicitly passing state around.\n\nRefactor the code to move the options into `cmd_fetch()`. Move variables\nthat were previously forced to be declared globally and which are only\nused by `cmd_fetch()` into function-local scope.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 197 ++++++++++++++++++++++++------------------------\n 1 file changed, 100 insertions(+), 97 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e15d43dc1e..820ec7285c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -75,13 +75,12 @@ static int fetch_prune_tags_config = -1; /* unspecified */\n static int prune_tags = -1; /* unspecified */\n #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */\n \n-static int all, append, dry_run, force, keep, multiple, update_head_ok;\n+static int append, dry_run, force, keep, update_head_ok;\n static int write_fetch_head = 1;\n static int verbosity, deepen_relative, set_upstream, refetch;\n static int progress = -1;\n-static int enable_auto_gc = 1;\n-static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;\n-static int max_jobs = -1, submodule_fetch_jobs_config = -1;\n+static int tags = TAGS_DEFAULT, update_shallow, deepen;\n+static int submodule_fetch_jobs_config = -1;\n static int fetch_parallel_config = 1;\n static int atomic_fetch;\n static enum transport_family family;\n@@ -92,17 +91,11 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n-static const char *submodule_prefix = \"\";\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n-static int fetch_write_commit_graph = -1;\n-static int stdin_refspecs = 0;\n-static int negotiate_only;\n \n static int git_fetch_config(const char *k, const char *v, void *cb)\n {\n@@ -160,92 +153,6 @@ static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-static struct option builtin_fetch_options[] = {\n-\tOPT__VERBOSITY(&verbosity),\n-\tOPT_BOOL(0, \"all\", &all,\n-\t\t N_(\"fetch from all remotes\")),\n-\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n-\t\t N_(\"set upstream for git pull/fetch\")),\n-\tOPT_BOOL('a', \"append\", &append,\n-\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n-\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n-\t\t N_(\"use atomic transaction to update references\")),\n-\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to upload pack on remote end\")),\n-\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n-\tOPT_BOOL('m', \"multiple\", &multiple,\n-\t\t N_(\"fetch from multiple remotes\")),\n-\tOPT_SET_INT('t', \"tags\", &tags,\n-\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n-\tOPT_SET_INT('n', NULL, &tags,\n-\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules fetched in parallel\")),\n-\tOPT_BOOL(0, \"prefetch\", &prefetch,\n-\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n-\tOPT_BOOL('p', \"prune\", &prune,\n-\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n-\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n-\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n-\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n-\t\t    N_(\"control recursive fetching of submodules\"),\n-\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"dry-run\", &dry_run,\n-\t\t N_(\"dry run\")),\n-\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n-\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n-\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n-\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n-\t\t    N_(\"allow updating of HEAD ref\")),\n-\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n-\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n-\t\t   N_(\"deepen history of shallow clone\")),\n-\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n-\t\t   N_(\"deepen history of shallow repository based on time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n-\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n-\t\t    N_(\"deepen history of shallow clone\")),\n-\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n-\t\t      N_(\"convert to a complete repository\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n-\t\t      N_(\"re-fetch without negotiating common commits\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n-\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n-\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n-\t\t   N_(\"default for recursive fetching of submodules \"\n-\t\t      \"(lower priority than config files)\"),\n-\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n-\t\t N_(\"accept refs that update .git/shallow\")),\n-\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n-\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n-\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV4),\n-\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV6),\n-\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n-\t\t\tN_(\"report that we have only objects reachable from this object\")),\n-\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n-\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n-\t\t N_(\"check for forced-updates on all updated branches\")),\n-\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n-\t\t N_(\"write the commit-graph after fetching\")),\n-\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n-\t\t N_(\"accept refspecs from stdin\")),\n-\tOPT_END()\n-};\n-\n static void unlock_pack(unsigned int flags)\n {\n \tif (gtransport)\n@@ -2147,13 +2054,109 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n-\tint i;\n \tconst char *bundle_uri;\n+\tconst char *submodule_prefix = \"\";\n \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n+\tint all = 0, multiple = 0;\n \tint result = 0;\n \tint prune_tags_ok = 1;\n+\tint enable_auto_gc = 1;\n+\tint unshallow = 0;\n+\tint max_jobs = -1;\n+\tint recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n+\tint recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n+\tint fetch_write_commit_graph = -1;\n+\tint stdin_refspecs = 0;\n+\tint negotiate_only = 0;\n+\tint i;\n+\n+\tstruct option builtin_fetch_options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_BOOL(0, \"all\", &all,\n+\t\t\t N_(\"fetch from all remotes\")),\n+\t\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n+\t\t\t N_(\"set upstream for git pull/fetch\")),\n+\t\tOPT_BOOL('a', \"append\", &append,\n+\t\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n+\t\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n+\t\t\t N_(\"use atomic transaction to update references\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to upload pack on remote end\")),\n+\t\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n+\t\tOPT_BOOL('m', \"multiple\", &multiple,\n+\t\t\t N_(\"fetch from multiple remotes\")),\n+\t\tOPT_SET_INT('t', \"tags\", &tags,\n+\t\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n+\t\tOPT_SET_INT('n', NULL, &tags,\n+\t\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules fetched in parallel\")),\n+\t\tOPT_BOOL(0, \"prefetch\", &prefetch,\n+\t\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n+\t\tOPT_BOOL('p', \"prune\", &prune,\n+\t\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n+\t\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n+\t\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n+\t\t\t    N_(\"control recursive fetching of submodules\"),\n+\t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n+\t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n+\t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n+\t\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n+\t\t\t    N_(\"allow updating of HEAD ref\")),\n+\t\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n+\t\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n+\t\t\t   N_(\"deepen history of shallow clone\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n+\t\t\t   N_(\"deepen history of shallow repository based on time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n+\t\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n+\t\t\t    N_(\"deepen history of shallow clone\")),\n+\t\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n+\t\t\t      N_(\"convert to a complete repository\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n+\t\t\t      N_(\"re-fetch without negotiating common commits\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n+\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n+\t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n+\t\t\t   N_(\"default for recursive fetching of submodules \"\n+\t\t\t      \"(lower priority than config files)\"),\n+\t\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n+\t\t\t N_(\"accept refs that update .git/shallow\")),\n+\t\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n+\t\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV4),\n+\t\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV6),\n+\t\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n+\t\t\t\tN_(\"report that we have only objects reachable from this object\")),\n+\t\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n+\t\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n+\t\t\t N_(\"check for forced-updates on all updated branches\")),\n+\t\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n+\t\t\t N_(\"write the commit-graph after fetching\")),\n+\t\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n+\t\t\t N_(\"accept refspecs from stdin\")),\n+\t\tOPT_END()\n+\t};\n \n \tpacket_trace_identity(\"fetch\");\n \n-- \n2.40.1\n\n"},{"id":"476882","messageId":"24ae381950249fd5c1cd799b75dda1407495d9d4.1683636885.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683636885.git.ps@pks.im","subject":"[PATCH v4 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-09T13:02:33Z","receivedAt":"2023-05-09T13:03:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The output of git-fetch(1) is obviously designed for consumption by\nusers, only: we neatly columnize data, we abbreviate reference names, we\nprint neat arrows and we don't provide information about actual object\nIDs that have changed. This makes the output format basically unusable\nin the context of scripted invocations of git-fetch(1) that want to\nlearn about the exact changes that the command performs.\n\nIntroduce a new machine-parseable \"porcelain\" output format that is\nsupposed to fix this shortcoming. This output format is intended to\nprovide information about every reference that is about to be updated,\nthe old object ID that the reference has been pointing to and the new\nobject ID it will be updated to. Furthermore, the output format provides\nthe same flags as the human-readable format to indicate basic conditions\nfor each reference update like whether it was a fast-forward update, a\nbranch deletion, a rejected update or others.\n\nThe output format is quite simple:\n\n```\n<flag> <old-object-id> <new-object-id> <local-reference>\\n\n```\n\nWe assume two conditions which are generally true:\n\n    - The old and new object IDs have fixed known widths and cannot\n      contain spaces.\n\n    - References cannot contain newlines.\n\nWith these assumptions, the output format becomes unambiguously\nparseable. Furthermore, given that this output is designed to be\nconsumed by scripts, the machine-readable data is printed to stdout\ninstead of stderr like the human-readable output is. This is mostly done\nso that other data printed to stderr, like error messages or progress\nmeters, don't interfere with the parseable data.\n\nA notable ommission here is that the output format does not include the\nremote from which a reference was fetched, which might be important\ninformation especially in the context of multi-remote fetches. But as\nsuch a format would require us to print the remote for every single\nreference update due to parallelizable fetches it feels wasteful for the\nmost likely usecase, which is when fetching from a single remote.\n\nIn a similar spirit, a second restriction is that this cannot be used\nwith `--recurse-submodules`. This is because any reference updates would\nbe ambiguous without also printing the repository in which the update\nhappens.\n\nConsidering that both multi-remote and submodule fetches are user-facing\nfeatures, using them in conjunction with `--porcelain` that is intended\nfor scripting purposes is likely not going to be useful in the majority\nof cases. With that in mind these restrictions feel acceptable. If\nusecases for either of these come up in the future though it is easy\nenough to add a new \"porcelain-v2\" format that adds this information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/fetch-options.txt |   7 ++\n Documentation/git-fetch.txt     |   9 ++\n builtin/fetch.c                 |  91 +++++++++++++++++----\n t/t5574-fetch-output.sh         | 140 ++++++++++++++++++++++++++++++++\n 4 files changed, 229 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 622bd84768..41fc7ca3c6 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -78,6 +78,13 @@ linkgit:git-config[1].\n --dry-run::\n \tShow what would be done, without making any changes.\n \n+--porcelain::\n+\tPrint the output to standard output in an easy-to-parse format for\n+\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n++\n+This is incompatible with `--recurse-submodules=[yes|on-demand]` and takes\n+precedence over the `fetch.output` config option.\n+\n ifndef::git-pull[]\n --[no-]write-fetch-head::\n \tWrite the list of remote refs fetched in the `FETCH_HEAD`\ndiff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\nindex fba66f1460..f123139c58 100644\n--- a/Documentation/git-fetch.txt\n+++ b/Documentation/git-fetch.txt\n@@ -204,6 +204,15 @@ representing the status of a single ref. Each line is of the form:\n  <flag> <summary> <from> -> <to> [<reason>]\n -------------------------------\n \n+When using `--porcelain`, the output format is intended to be\n+machine-parseable. In contrast to the human-readable output formats it\n+thus prints to standard output instead of standard error. Each line is\n+of the form:\n+\n+-------------------------------\n+<flag> <old-object-id> <new-object-id> <local-reference>\n+-------------------------------\n+\n The status of up-to-date refs is shown only if the --verbose option is\n used.\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 820ec7285c..187c4d373c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,7 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_PORCELAIN,\n };\n \n struct display_state {\n@@ -745,6 +746,9 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\t/* We don't need to precompute anything here. */\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n@@ -815,8 +819,12 @@ static void print_compact(struct display_state *display_state,\n static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *summary, const char *error,\n \t\t\t       const char *remote, const char *local,\n+\t\t\t       const struct object_id *old_oid,\n+\t\t\t       const struct object_id *new_oid,\n \t\t\t       int summary_width)\n {\n+\tFILE *f = stderr;\n+\n \tif (verbosity < 0)\n \t\treturn;\n \n@@ -849,12 +857,17 @@ static void display_ref_update(struct display_state *display_state, char code,\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\tstrbuf_addf(&display_state->buf, \"%c %s %s %s\", code,\n+\t\t\t    oid_to_hex(old_oid), oid_to_hex(new_oid), local);\n+\t\tf = stdout;\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n-\tfputs(display_state->buf.buf, stderr);\n+\tfputs(display_state->buf.buf, f);\n }\n \n static int update_local_ref(struct ref *ref,\n@@ -872,7 +885,8 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -885,7 +899,8 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -896,12 +911,14 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -934,7 +951,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn r;\n \t}\n \n@@ -956,7 +974,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -968,12 +987,14 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1214,7 +1235,9 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n \t\t\t\t\t\t   rm->name,\n-\t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n+\t\t\t\t\t\t   \"FETCH_HEAD\",\n+\t\t\t\t\t\t   &rm->new_oid, &rm->old_oid,\n+\t\t\t\t\t\t   summary_width);\n \t\t\t}\n \t\t}\n \t}\n@@ -1354,6 +1377,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\tdisplay_ref_update(display_state, '-', _(\"[deleted]\"), NULL,\n \t\t\t\t\t   _(\"(none)\"), ref->name,\n+\t\t\t\t\t   &ref->new_oid, &ref->old_oid,\n \t\t\t\t\t   summary_width);\n \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n \t\t}\n@@ -1786,7 +1810,8 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n \treturn 1;\n }\n \n-static void add_options_to_argv(struct strvec *argv)\n+static void add_options_to_argv(struct strvec *argv,\n+\t\t\t\tenum display_format format)\n {\n \tif (dry_run)\n \t\tstrvec_push(argv, \"--dry-run\");\n@@ -1822,6 +1847,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--ipv6\");\n \tif (!write_fetch_head)\n \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n+\tif (format == DISPLAY_FORMAT_PORCELAIN)\n+\t\tstrvec_pushf(argv, \"--porcelain\");\n }\n \n /* Fetch multiple remotes in parallel */\n@@ -1830,6 +1857,7 @@ struct parallel_fetch_state {\n \tconst char **argv;\n \tstruct string_list *remotes;\n \tint next, result;\n+\tenum display_format format;\n };\n \n static int fetch_next_remote(struct child_process *cp,\n@@ -1849,7 +1877,7 @@ static int fetch_next_remote(struct child_process *cp,\n \tstrvec_push(&cp->args, remote);\n \tcp->git_cmd = 1;\n \n-\tif (verbosity >= 0)\n+\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n \t\tprintf(_(\"Fetching %s\\n\"), remote);\n \n \treturn 1;\n@@ -1881,7 +1909,8 @@ static int fetch_finished(int result, struct strbuf *out,\n \treturn 0;\n }\n \n-static int fetch_multiple(struct string_list *list, int max_children)\n+static int fetch_multiple(struct string_list *list, int max_children,\n+\t\t\t  enum display_format format)\n {\n \tint i, result = 0;\n \tstruct strvec argv = STRVEC_INIT;\n@@ -1894,10 +1923,10 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \tstrvec_pushl(&argv, \"fetch\", \"--append\", \"--no-auto-gc\",\n \t\t     \"--no-write-commit-graph\", NULL);\n-\tadd_options_to_argv(&argv);\n+\tadd_options_to_argv(&argv, format);\n \n \tif (max_children != 1 && list->nr != 1) {\n-\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n+\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0, format };\n \t\tconst struct run_process_parallel_opts opts = {\n \t\t\t.tr2_category = \"fetch\",\n \t\t\t.tr2_label = \"parallel/fetch\",\n@@ -1921,7 +1950,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \t\t\tstrvec_pushv(&cmd.args, argv.v);\n \t\t\tstrvec_push(&cmd.args, name);\n-\t\t\tif (verbosity >= 0)\n+\t\t\tif (verbosity >= 0 && format != DISPLAY_FORMAT_PORCELAIN)\n \t\t\t\tprintf(_(\"Fetching %s\\n\"), name);\n \t\t\tcmd.git_cmd = 1;\n \t\t\tif (run_command(&cmd)) {\n@@ -2052,6 +2081,13 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \treturn exit_code;\n }\n \n+static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n+{\n+\tenum display_format *format = opt->value;\n+\t*format = DISPLAY_FORMAT_PORCELAIN;\n+\treturn 0;\n+}\n+\n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n \tconst char *bundle_uri;\n@@ -2104,6 +2140,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n \t\t\t N_(\"dry run\")),\n+\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n+\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n@@ -2222,6 +2260,23 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tfetch_config_from_gitmodules(sfjc, rs);\n \t}\n \n+\tif (display_format == DISPLAY_FORMAT_PORCELAIN) {\n+\t\tswitch (recurse_submodules_cli) {\n+\t\tcase RECURSE_SUBMODULES_OFF:\n+\t\tcase RECURSE_SUBMODULES_DEFAULT:\n+\t\t\t/*\n+\t\t\t * Reference updates in submodules would be ambiguous\n+\t\t\t * in porcelain mode, so we reject this combination.\n+\t\t\t */\n+\t\t\trecurse_submodules = RECURSE_SUBMODULES_OFF;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--porcelain\", \"--recurse-submodules\");\n+\t\t}\n+\t}\n+\n \tif (negotiate_only && !negotiation_tip.nr)\n \t\tdie(_(\"--negotiate-only needs one or more --negotiation-tip=*\"));\n \n@@ -2341,7 +2396,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\tmax_children = fetch_parallel_config;\n \n \t\t/* TODO should this also die if we have a previous partial-clone? */\n-\t\tresult = fetch_multiple(&list, max_children);\n+\t\tresult = fetch_multiple(&list, max_children, display_format);\n \t}\n \n \n@@ -2363,7 +2418,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tif (max_children < 0)\n \t\t\tmax_children = fetch_parallel_config;\n \n-\t\tadd_options_to_argv(&options);\n+\t\tadd_options_to_argv(&options, display_format);\n \t\tresult = fetch_submodules(the_repository,\n \t\t\t\t\t  &options,\n \t\t\t\t\t  submodule_prefix,\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 6e0f7e0046..695624ac08 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -56,6 +56,122 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\t# Set up a bunch of references that we can use to demonstrate different\n+\t# kinds of flag symbols in the output format.\n+\tMAIN_OLD=$(git rev-parse HEAD) &&\n+\tgit branch \"fast-forward\" &&\n+\tgit branch \"deleted-branch\" &&\n+\tgit checkout -b force-updated &&\n+\ttest_commit --no-tag force-update-old &&\n+\tFORCE_UPDATED_OLD=$(git rev-parse HEAD) &&\n+\tgit checkout main &&\n+\n+\t# Clone and pre-seed the repositories. We fetch references into two\n+\t# namespaces so that we can test that rejected and force-updated\n+\t# references are reported properly.\n+\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n+\tgit clone . porcelain &&\n+\tgit -C porcelain fetch origin $refspecs &&\n+\n+\t# Now that we have set up the client repositories we can change our\n+\t# local references.\n+\tgit branch new-branch &&\n+\tgit branch -d deleted-branch &&\n+\tgit checkout fast-forward &&\n+\ttest_commit --no-tag fast-forward-new &&\n+\tFAST_FORWARD_NEW=$(git rev-parse HEAD) &&\n+\tgit checkout force-updated &&\n+\tgit reset --hard HEAD~ &&\n+\ttest_commit --no-tag force-update-new &&\n+\tFORCE_UPDATED_NEW=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n+\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n+\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\tEOF\n+\n+\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n+\t# and non-dry-run fetches produces the same output. Execution of the\n+\t# fetch is expected to fail as we have a rejected reference update.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --dry-run --prune origin $refspecs >actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# And now we perform a non-dry-run fetch.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --prune origin $refspecs >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain with multiple remotes' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tgit switch --create multiple-remotes &&\n+\tgit clone . porcelain &&\n+\tgit -C porcelain remote add second-remote \"$PWD\" &&\n+\tgit -C porcelain fetch second-remote &&\n+\n+\ttest_commit --no-tag multi-commit &&\n+\told_commit=$(git rev-parse HEAD~) &&\n+\tnew_commit=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t  $old_commit $new_commit refs/remotes/origin/multiple-remotes\n+\t  $old_commit $new_commit refs/remotes/second-remote/multiple-remotes\n+\tEOF\n+\n+\tgit -C porcelain fetch --porcelain --all >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain refuses to work with submodules' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tcat >expect <<-EOF &&\n+\tfatal: options ${SQ}--porcelain${SQ} and ${SQ}--recurse-submodules${SQ} cannot be used together\n+\tEOF\n+\n+\tgit init porcelain &&\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=yes 2>stderr &&\n+\ttest_cmp expect stderr &&\n+\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=on-demand 2>stderr &&\n+\ttest_cmp expect stderr\n+'\n+\n+test_expect_success 'fetch porcelain overrides fetch.output config' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tgit switch --create config-override &&\n+\tgit clone . porcelain &&\n+\ttest_commit new-commit &&\n+\told_commit=$(git rev-parse HEAD~) &&\n+\tnew_commit=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t  $old_commit $new_commit refs/remotes/origin/config-override\n+\t* $ZERO_OID $new_commit refs/tags/new-commit\n+\tEOF\n+\n+\tgit -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&\n+\ttest_must_be_empty stderr &&\n+\ttest_cmp expect stdout\n+'\n+\n test_expect_success 'fetch output with HEAD' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n@@ -85,6 +201,30 @@ test_expect_success 'fetch output with HEAD' '\n \ttest_cmp expect actual.err\n '\n \n+test_expect_success 'fetch porcelain output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\tCOMMIT_ID=$(git rev-parse HEAD) &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD:foo >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD:foo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476890","messageId":"xmqq1qjpz96s.fsf@gitster.g","threadId":"59619","inReplyTo":"d82b42ed345ac7b482bf5dd96456f131ecb8c746.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T17:49:47Z","receivedAt":"2023-05-09T17:49:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When running `git fetch --no-recurse-submodules`, the exectation is that\n> we don't fetch any submodules. And while this works for fetches of a\n> single remote, it doesn't when fetching multiple remotes at once. The\n> result is that we do recurse into submodules even though the user has\n> explicitly asked us not to.\n>\n> This is because while we pass on `--recurse-submodules={yes,on-demand}`\n> if specified by the user, we don't pass on `--no-recurse-submodules` to\n> the subprocess spawned to perform the submodule fetch.\n>\n> Fix this by also forwarding this flag as expected.\n\nMakes sense.\n\n> +test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n> +\ttest_when_finished \"rm -rf src_clone\" &&\n> +\n> +\tgit clone --recurse-submodules src src_clone &&\n> +\t(\n> +\t\tcd src_clone &&\n> +\t\tgit remote add secondary ../src &&\n> +\t\tgit config submodule.recurse true &&\n\nThe above two is essential to this test; we are interested in making\nsure that --no-recurse-submodules is propagated down even when the\n\"--all\" option is used, and we want another remote for that.  We set\nthe default to recurse, so that passing \"--no-recurse-submodules\"\nwould defeat it, but just refraining to pass \"--recurse-submodules\"\nwould cause us to recurse.\n\n> +\t\tgit config fetch.parallel 0 &&\n\nIs this necessary for the purpose of the test, though?  It should\nnot hurt, but we do not require the end-users to set it in real life\nfor the parallel fetching to work, either, right?\n\nJust being curious.\n\n> +\t\tgit fetch --all --no-recurse-submodules 2>../actual\n> +\t) &&\n> +\n> +\tcat >expect <<-EOF &&\n> +\tFrom ../src\n> +\t * [new branch]      master     -> secondary/master\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nIn the context of a series that attempts to introduce a new stable\noutput format for machine consumption, which implies the traditional\noutput can change to match human users' preference, this test feels\na bit brittle, but let's wait until the end of the series to judge\nthat.\n\nLooking good.  Thanks.\n\n>  test_done\n"},{"id":"476908","messageId":"xmqqv8h1xu7q.fsf@gitster.g","threadId":"59619","inReplyTo":"006ea93afbef8742a823a9444d2b71785a9b4ec3.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 3/8] fetch: add a test to exercise invalid output formats","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T17:58:33Z","receivedAt":"2023-05-09T17:58:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Add a testcase that exercises the logic when an invalid output format is\n> passed via the `fetch.output` configuration.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t5574-fetch-output.sh | 19 +++++++++++++++++++\n>  1 file changed, 19 insertions(+)\n\nIt makes perfect sense to make sure that invalid input gets rejected\nand the command exits with non-zero status, and it is probably a\ngood thing that the end-user sees a message that explains why the\nparticular input is rejected (even though it adds one more thing\nthat needs to be updated when the message gets reworded).\n\nBut do we need to insist on no output to the standard output stream\nwhen the command errors out?\n\nOther than that, looking good.\n\nTHanks.\n\n\n> diff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\n> index f91b654d38..a09750d225 100755\n> --- a/t/t5574-fetch-output.sh\n> +++ b/t/t5574-fetch-output.sh\n> @@ -7,6 +7,25 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./test-lib.sh\n>  \n> +test_expect_success 'fetch with invalid output format configuration' '\n> +\ttest_when_finished \"rm -rf clone\" &&\n> +\tgit clone . clone &&\n> +\n> +\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual.out 2>actual.err &&\n> +\tcat >expect <<-EOF &&\n> +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n> +\tEOF\n> +\ttest_must_be_empty actual.out &&\n> +\ttest_cmp expect actual.err &&\n> +\n> +\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual.out 2>actual.err &&\n> +\tcat >expect <<-EOF &&\n> +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n> +\tEOF\n> +\ttest_must_be_empty actual.out &&\n> +\ttest_cmp expect actual.err\n> +'\n> +\n>  test_expect_success 'fetch aligned output' '\n>  \tgit clone . full-output &&\n>  \ttest_commit looooooooooooong-tag &&\n"},{"id":"476909","messageId":"kl6l5y911ht4.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"xmqq1qjpz96s.fsf@gitster.g","subject":"Re: [PATCH v4 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-09T18:27:35Z","receivedAt":"2023-05-09T18:27:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> +\t\tgit config fetch.parallel 0 &&\n>\n> Is this necessary for the purpose of the test, though?  It should\n> not hurt, but we do not require the end-users to set it in real life\n> for the parallel fetching to work, either, right?\n\nIIUC it would make the test output deterministic if we fetched from both\nremotes. That doesn't happen here though, so I guess it's not doing\nanything right now.\n\n>> +\t\tgit fetch --all --no-recurse-submodules 2>../actual\n>> +\t) &&\n>> +\n>> +\tcat >expect <<-EOF &&\n>> +\tFrom ../src\n>> +\t * [new branch]      master     -> secondary/master\n>> +\tEOF\n>> +\ttest_cmp expect actual\n>> +'\n>\n> In the context of a series that attempts to introduce a new stable\n> output format for machine consumption, which implies the traditional\n> output can change to match human users' preference, this test feels\n> a bit brittle, but let's wait until the end of the series to judge\n> that.\n\nI also find it a bit brittle to assert on the whole output when this\ntest is about checking that we do not fetch the superproject.\n\nIs there a reason you didn't go with the \"grep for submodule lines\"\napproach in the previous tests? If it's about catching regressions, IMO\nyour PATCH 2/8 does a good enough job of doing that.\n\nWondering out loud, I wonder if it makes sense for us to make a bigger\ndistinction between \"tests whose purpose is to guard against unexpected\nchanges in output\" (i.e. snapshot tests) vs \"tests that happen to use\noutput as a way to assert behavior\" (i.e. 'regular' behavioral tests).\nMany GUI app codebases have such a distinction and have different best\npractices around them.\n"},{"id":"476915","messageId":"xmqqednpxq27.fsf@gitster.g","threadId":"59619","inReplyTo":"e599ea6d335d16b45fa75b223ea8db93e46c627d.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 4/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T19:28:16Z","receivedAt":"2023-05-09T19:28:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> But it is not intended when displaying the updated references and would\n> cause us to miss the left-hand side of the displayed reference update:\n>\n> ```\n> $ git fetch origin HEAD:foo\n> From https://github.com/git/git\n>  * [new ref]                          -> foo\n> ```\n> The HEAD string is clearly missing from the left-hand side of the arrow,\n> which is further stressed by the point that the following commands show\n> the left-hand side as expected:\n>\n> ```\n> $ git fetch origin HEAD\n> From https://github.com/git/git\n>  * branch                  HEAD       -> FETCH_HEAD\n\nI do not mind being explicit and showing HEAD in this case for the\nsake of consistency.\n\nBut speaking for the past developers, it was deliberate to omit what\nis common from the output to make it more terse, IIRC, and I think\nit is unfair to call it a \"BUG\".\n\nBack when we wrote git-fetch-script, the output was a lot more\nverbose, and through efforts like 165f3902 (git-fetch: more terse\nfetch output, 2007-11-03) and numerous others over time, we got to\nthe current output.\n\n> Note that this patch also changes formatting of the block that computes\n> the \"kind\" and \"what\" variables. This is done on purpose so that it is\n> part of the diff, hopefully making the change easier to comprehend.\n\nJust to help readers, \"kind\" is the category like branch, tag,\netc. and \"what\" is the concrete name like 'master' and 'foo'.\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 08d7fc7233..6aecf549e8 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n>  \t}\n>  \n>  \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n> +\tremote = prettify_refname(remote);\n> +\tlocal = prettify_refname(local);\n>  \n>  \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n>  \tif (!display_state->compact_format)\n> -\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n> +\t\tprint_remote_to_local(display_state, remote, local);\n>  \telse\n> -\t\tprint_compact(display_state, remote, prettify_refname(local));\n> +\t\tprint_compact(display_state, remote, local);\n>  \tif (error)\n>  \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n>  \tstrbuf_addch(&display_state->buf, '\\n');\n> @@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n>  static int update_local_ref(struct ref *ref,\n>  \t\t\t    struct ref_transaction *transaction,\n>  \t\t\t    struct display_state *display_state,\n> -\t\t\t    const char *remote, const struct ref *remote_ref,\n> +\t\t\t    const struct ref *remote_ref,\n>  \t\t\t    int summary_width)\n>  {\n>  \tstruct commit *current = NULL, *updated;\n> @@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n>  \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n>  \t\tif (verbosity > 0)\n>  \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n> -\t\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n\nMakes sense.  The variable \"remote\" (now removed) holds what to\nwrite to FETCH_HEAD to be used to formulate a merge message by the\ncaller, but this function is purely to report the ref updates and\nhas no need to have access to that information.\n\n> @@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n>  \t\t\t\tkind = \"\";\n>  \t\t\t\twhat = \"\";\n> -\t\t\t}\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n>  \t\t\t\tkind = \"branch\";\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n>  \t\t\t\tkind = \"tag\";\n> -\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n> +\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n>  \t\t\t\tkind = \"remote-tracking branch\";\n> -\t\t\telse {\n> +\t\t\t} else {\n>  \t\t\t\tkind = \"\";\n>  \t\t\t\twhat = rm->name;\n>  \t\t\t}\n\nThis is a bit noisier than necessary.  It took me a while until I\nrealized that this hunk is a no-op.\n\n> @@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\t\t\t  display_state->url_len);\n>  \n>  \t\t\tif (ref) {\n> -\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n> +\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n>  \t\t\t\t\t\t       rm, summary_width);\n>  \t\t\t\tfree(ref);\n\nGood.\n\n> @@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n>  \t\t\t\t */\n>  \t\t\t\tdisplay_ref_update(display_state, '*',\n>  \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n> -\t\t\t\t\t\t   *what ? what : \"HEAD\",\n> +\t\t\t\t\t\t   rm->name,\n>  \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n\nGood, too.  The original cleared \"what\" and then to compensate for\nthat had a logic to turn it back to \"HEAD\", but that is all gone by\npassing rm->name down.  \n\nI think we could pass \"rm\" and leave it to display_ref_update() what\nstring to use, if we wanted to further refine the output later.\n\nThen somebody in the future may even want to see \"HEAD\" to be shown\nas an empty string and that can all be done in display_ref_update().\nIt would fix the inconsistency that \"git fetch origin HEAD\" reports\n\"HEAD -> FETCH_HEAD\" by hiding \"HEAD\" just like the case where\nfetching \"HEAD:foo\" does, going in the other direction, I would\nthink.\n\nThanks.\n"},{"id":"476920","messageId":"xmqq5y91xnou.fsf@gitster.g","threadId":"59619","inReplyTo":"80ac00b0c4b0fde61a3edf48d996e3a60327a54d.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 5/8] fetch: introduce `display_format` enum","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T20:19:29Z","receivedAt":"2023-05-09T20:19:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We currently have two different display formats in git-fetch(1) with the\n> \"full\" and \"compact\" formats. This is tracked with a boolean value that\n> simply denotes whether the display format is supposed to be compacted\n> or not. This works reasonably well while there are only two formats, but\n> we're about to introduce another format that will make this a bit more\n> awkward to use.\n>\n> Introduce a `enum display_format` that is more readily extensible.\n\nMakes sense.\n\n> +enum display_format {\n> +\tDISPLAY_FORMAT_UNKNOWN = 0,\n> +\tDISPLAY_FORMAT_FULL,\n> +\tDISPLAY_FORMAT_COMPACT,\n> +};\n>\n>  struct display_state {\n>  \tstruct strbuf buf;\n>  \n>  \tint refcol_width;\n> -\tint compact_format;\n> +\tenum display_format format;\n\nOK.  Preparatory conversion without adding anything new.\n\n> @@ -809,31 +814,42 @@ static void display_state_init(struct display_state *display_state, struct ref *\n>  \n>  \tgit_config_get_string_tmp(\"fetch.output\", &format);\n>  \tif (!strcasecmp(format, \"full\"))\n> -\t\tdisplay_state->compact_format = 0;\n> +\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n>  \telse if (!strcasecmp(format, \"compact\"))\n> -\t\tdisplay_state->compact_format = 1;\n> +\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n>  \telse\n>  \t\tdie(_(\"invalid value for '%s': '%s'\"),\n>  \t\t    \"fetch.output\", format);\n\nNaturally.\n\n> -\tdisplay_state->refcol_width = 10;\n> -\tfor (rm = ref_map; rm; rm = rm->next) {\n> -\t\tint width;\n> +\tswitch (display_state->format) {\n> +\tcase DISPLAY_FORMAT_FULL:\n> +\tcase DISPLAY_FORMAT_COMPACT: {\n> +\t\tstruct ref *rm;\n>  \n> -\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n> -\t\t    !rm->peer_ref ||\n> -\t\t    !strcmp(rm->name, \"HEAD\"))\n> -\t\t\tcontinue;\n> +\t\tdisplay_state->refcol_width = 10;\n> +\t\tfor (rm = ref_map; rm; rm = rm->next) {\n> +\t\t\tint width;\n>  \n> -\t\twidth = refcol_width(rm, display_state->compact_format);\n> +\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n> +\t\t\t    !rm->peer_ref ||\n> +\t\t\t    !strcmp(rm->name, \"HEAD\"))\n> +\t\t\t\tcontinue;\n>  \n> -\t\t/*\n> -\t\t * Not precise calculation for compact mode because '*' can\n> -\t\t * appear on the left hand side of '->' and shrink the column\n> -\t\t * back.\n> -\t\t */\n> -\t\tif (display_state->refcol_width < width)\n> -\t\t\tdisplay_state->refcol_width = width;\n> +\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n> +\n> +\t\t\t/*\n> +\t\t\t * Not precise calculation for compact mode because '*' can\n> +\t\t\t * appear on the left hand side of '->' and shrink the column\n> +\t\t\t * back.\n> +\t\t\t */\n> +\t\t\tif (display_state->refcol_width < width)\n> +\t\t\t\tdisplay_state->refcol_width = width;\n> +\t\t}\n> +\n> +\t\tbreak;\n> +\t}\n> +\tdefault:\n> +\t\tBUG(\"unexpected display format %d\", display_state->format);\n>  \t}\n\nDue to reindentation, the patch is noisier than what it does (which\nshould be \"nothing, other than allowing another value in the .format\nmember\").\n\nIt makes me wonder if it would make it easier to read to move the\nbulk of this code to a helper function.  If we are to give a name to\nwhat is being done in the above hunk, what would it be?  It computes\ndisplay->refcol_width in which all records would fit, but presumably\nif we are to add more things to be shown per ref and align them in a\nsimlar way, we would compute widths for these other things there as\nwell.  Perhaps compute_display_alignment() or somesuch?\n\n"},{"id":"476924","messageId":"xmqqo7mtw8d8.fsf@gitster.g","threadId":"59619","inReplyTo":"826b8b7bc0d7d6a76f6fd19d8f4a8460af61e9cf.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 6/8] fetch: move display format parsing into main function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T20:35:47Z","receivedAt":"2023-05-09T20:35:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We're about to introduce a output format though that is intended to be\n> parseable by machines, for example inside of a script. In that case it\n> becomes a bit awkward of an interface if you have to call git-fetch(1)\n> with the `fetch.output` config key set. We're thus going to introduce a\n> new `--output-format` switch for git-fetch(1) so that the output format\n> can be configured more directly.\n\nGood.  I was wondering about that code in the context of the\nprevious patch, especially the error message had a hard-coded\nassumption that it comes from a configuration variable.\n\nAnd the changes to display_state_init() to lift the responsibility\nof finding and validating .format out of the function, and the\nchanges to intermediate functions to pass the .format through the\ncallchain, are all expected and there was nothing questionable.\n\n> @@ -2157,6 +2149,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n>  {\n>  \tint i;\n>  \tconst char *bundle_uri;\n> +\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n>  \tstruct string_list list = STRING_LIST_INIT_DUP;\n>  \tstruct remote *remote = NULL;\n>  \tint result = 0;\n> @@ -2183,6 +2176,19 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n>  \targc = parse_options(argc, argv, prefix,\n>  \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n>  \n> +\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n> +\t\tconst char *format = \"full\";\n> +\n> +\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n> +\t\tif (!strcasecmp(format, \"full\"))\n> +\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n> +\t\telse if (!strcasecmp(format, \"compact\"))\n> +\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n> +\t\telse\n> +\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n> +\t\t\t    \"fetch.output\", format);\n> +\t}\n\nOK, but isn't the usual way to do this to have configuration parser\nbefore parse_options() and then let parse_options() override\nwhatever display_format set by it?\n\nThat way, we do not have to worry about DISPLAY_FORMAT_UNKNOWN at\nall.  Just initialize the variable to whatever default format at the\nbeginning of this function, read \"fetch.output\" to override it if\nthe configuration exists, and then let parse_options() to handle\n\"--output-format\" or \"--porcelain\" or whatever to further override\nit.\n\nThanks.\n"},{"id":"476926","messageId":"xmqqjzxhw802.fsf@gitster.g","threadId":"59619","inReplyTo":"24ae381950249fd5c1cd799b75dda1407495d9d4.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-09T20:43:41Z","receivedAt":"2023-05-09T20:43:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Considering that both multi-remote and submodule fetches are user-facing\n> features, using them in conjunction with `--porcelain` that is intended\n> for scripting purposes is likely not going to be useful in the majority\n> of cases. With that in mind these restrictions feel acceptable. If\n> usecases for either of these come up in the future though it is easy\n> enough to add a new \"porcelain-v2\" format that adds this information.\n\nTwo steps ago, the proposed log message still mentioned \"--output-format\",\nwhich may want to be proofread again and revised.\n\n> @@ -1786,7 +1810,8 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n>  \treturn 1;\n>  }\n>  \n> -static void add_options_to_argv(struct strvec *argv)\n> +static void add_options_to_argv(struct strvec *argv,\n> +\t\t\t\tenum display_format format)\n>  {\n>  \tif (dry_run)\n>  \t\tstrvec_push(argv, \"--dry-run\");\n> @@ -1822,6 +1847,8 @@ static void add_options_to_argv(struct strvec *argv)\n>  \t\tstrvec_push(argv, \"--ipv6\");\n>  \tif (!write_fetch_head)\n>  \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n> +\tif (format == DISPLAY_FORMAT_PORCELAIN)\n> +\t\tstrvec_pushf(argv, \"--porcelain\");\n>  }\n\nHmph.  \n\n[PATCH 9/8] may want to also introduce and pass down the\n\"--output-format=full/compact\" option, but that is clearly outside\nof the scope of this step.\n\n> +static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n> +{\n> +\tenum display_format *format = opt->value;\n> +\t*format = DISPLAY_FORMAT_PORCELAIN;\n> +\treturn 0;\n> +}\n\nLack of \"if (unset) ...\" worries me.  Shouldn't the code allow\n\n\tgit fetch --porcelain --no-porcelain\n\nto defeat an earlier one and revert back to the default?\n\nOther than that the changes in this step were mostly expected and I\ndidn't spot anything glaringly wrong, but I didn't spend as much\ntime as I did for the other steps.\n\nThanks.\n\n"},{"id":"476934","messageId":"kl6l1qjp16k6.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"826b8b7bc0d7d6a76f6fd19d8f4a8460af61e9cf.1683636885.git.ps@pks.im","subject":"Re: [PATCH v4 6/8] fetch: move display format parsing into main function","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-09T22:30:33Z","receivedAt":"2023-05-09T22:31:23Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> with the `fetch.output` config key set. We're thus going to introduce a\n> new `--output-format` switch for git-fetch(1) so that the output format\n> can be configured more directly.\n\nThis is stale as of v3, since isn't named --output-format any more. Let\nme see if there are other instances of this.\n\n(I should have caught this earlier; I only saw it because Junio quoted\nit in a reply of his.)\n"},{"id":"476957","messageId":"02ee4fab7e8b46bb04b1e7509bd752144ac77a12.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 1/9] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:02Z","receivedAt":"2023-05-10T12:34:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When running `git fetch --no-recurse-submodules`, the exectation is that\nwe don't fetch any submodules. And while this works for fetches of a\nsingle remote, it doesn't when fetching multiple remotes at once. The\nresult is that we do recurse into submodules even though the user has\nexplicitly asked us not to.\n\nThis is because while we pass on `--recurse-submodules={yes,on-demand}`\nif specified by the user, we don't pass on `--no-recurse-submodules` to\nthe subprocess spawned to perform the submodule fetch.\n\nFix this by also forwarding this flag as expected.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c             |  2 ++\n t/t5526-fetch-submodules.sh | 13 +++++++++++++\n 2 files changed, 15 insertions(+)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c310d89878..08d7fc7233 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1876,6 +1876,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--keep\");\n \tif (recurse_submodules == RECURSE_SUBMODULES_ON)\n \t\tstrvec_push(argv, \"--recurse-submodules\");\n+\telse if (recurse_submodules == RECURSE_SUBMODULES_OFF)\n+\t\tstrvec_push(argv, \"--no-recurse-submodules\");\n \telse if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)\n \t\tstrvec_push(argv, \"--recurse-submodules=on-demand\");\n \tif (tags == TAGS_SET)\ndiff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\nindex dcdbe26a08..26e933f93a 100755\n--- a/t/t5526-fetch-submodules.sh\n+++ b/t/t5526-fetch-submodules.sh\n@@ -1180,4 +1180,17 @@ test_expect_success 'fetch --all with --recurse-submodules with multiple' '\n \ttest_line_count = 2 fetch-subs\n '\n \n+test_expect_success \"fetch --all with --no-recurse-submodules only fetches superproject\" '\n+\ttest_when_finished \"rm -rf src_clone\" &&\n+\n+\tgit clone --recurse-submodules src src_clone &&\n+\t(\n+\t\tcd src_clone &&\n+\t\tgit remote add secondary ../src &&\n+\t\tgit config submodule.recurse true &&\n+\t\tgit fetch --all --no-recurse-submodules 2>../fetch-log\n+\t) &&\n+\t! grep \"Fetching submodule\" fetch-log\n+'\n+\n test_done\n-- \n2.40.1\n\n"},{"id":"476958","messageId":"cover.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1681906948.git.ps@pks.im","subject":"[PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:33:58Z","receivedAt":"2023-05-10T12:34:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fourth version of my patch series to introduce a\nmachine-parseable output format for git-fetch(1). It applies on top of\ne9dffbc7f1 (Merge branch 'ps/fetch-ref-update-reporting', 2023-04-06).\n\nChanges compared to v4:\n\n    - Patch 1/9: Simplified the test as proposed by Junio and Glen.\n\n    - Patch 3/9: Added a test to verify that `git fetch -c fetch.output`\n      without a value set fails as expected. Also dropped the tests that\n      checked whether stdout was empty.\n\n    - Patch 4/9: Reformulated the commit message to treat the missing\n      left-hand side of displayed references as an inconsistency instead\n      of a bug. I've also added a testcase to verify that direct OID\n      fetches continue to work as expected.\n\n    - Patch 5/9: New patch that makes calculation of the table width for\n      displayed reference updates self-contained in `refcol_width()`.\n      This is a preparatory refactoring that should make patch 6/9\n      easier to review.\n\n    - Patch 7/9: Refactored the code to parse the \"fetch.output\" config\n      variable inside of `git_fetch_config()` before we parse command\n      line options. Also fixed that the commit message was still\n      referring to `--output-format=porcelain` instead of the new\n      `--porcelain` switch.\n\n    - Patch 9/9: The `--porcelain` option is now a simple `OPT_BOOL()`\n      that can be negated. Added a test that `--no-porcelain` works as\n      expected.\n\nThanks for your feedback, Junio and Glen!\n\nPatrick\n\nPatrick Steinhardt (9):\n  fetch: fix `--no-recurse-submodules` with multi-remote fetches\n  fetch: split out tests for output format\n  fetch: add a test to exercise invalid output formats\n  fetch: print left-hand side when fetching HEAD:foo\n  fetch: refactor calculation of the display table width\n  fetch: introduce `display_format` enum\n  fetch: lift up parsing of \"fetch.output\" config variable\n  fetch: move option related variables into main function\n  fetch: introduce machine-parseable \"porcelain\" output format\n\n Documentation/fetch-options.txt |   7 +\n Documentation/git-fetch.txt     |   9 +\n builtin/fetch.c                 | 490 +++++++++++++++++++-------------\n t/t5510-fetch.sh                |  53 ----\n t/t5526-fetch-submodules.sh     |  13 +\n t/t5574-fetch-output.sh         | 293 +++++++++++++++++++\n 6 files changed, 611 insertions(+), 254 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\nRange-diff against v4:\n 1:  d82b42ed34 !  1:  02ee4fab7e fetch: fix `--no-recurse-submodules` with multi-remote fetches\n    @@ t/t5526-fetch-submodules.sh: test_expect_success 'fetch --all with --recurse-sub\n     +\t\tcd src_clone &&\n     +\t\tgit remote add secondary ../src &&\n     +\t\tgit config submodule.recurse true &&\n    -+\t\tgit config fetch.parallel 0 &&\n    -+\t\tgit fetch --all --no-recurse-submodules 2>../actual\n    ++\t\tgit fetch --all --no-recurse-submodules 2>../fetch-log\n     +\t) &&\n    -+\n    -+\tcat >expect <<-EOF &&\n    -+\tFrom ../src\n    -+\t * [new branch]      master     -> secondary/master\n    -+\tEOF\n    -+\ttest_cmp expect actual\n    ++\t! grep \"Fetching submodule\" fetch-log\n     +'\n     +\n      test_done\n 2:  33112dc51a =  2:  e459d8a1ec fetch: split out tests for output format\n 3:  006ea93afb !  3:  d503c425fe fetch: add a test to exercise invalid output formats\n    @@ t/t5574-fetch-output.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +\ttest_when_finished \"rm -rf clone\" &&\n     +\tgit clone . clone &&\n     +\n    -+\ttest_must_fail git -C clone -c fetch.output= fetch origin >actual.out 2>actual.err &&\n    ++\ttest_must_fail git -C clone -c fetch.output fetch origin 2>actual.err &&\n    ++\tcat >expect <<-EOF &&\n    ++\terror: missing value for ${SQ}fetch.output${SQ}\n    ++\tfatal: unable to parse ${SQ}fetch.output${SQ} from command-line config\n    ++\tEOF\n    ++\ttest_cmp expect actual.err &&\n    ++\n    ++\ttest_must_fail git -C clone -c fetch.output= fetch origin 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n     +\tEOF\n    -+\ttest_must_be_empty actual.out &&\n     +\ttest_cmp expect actual.err &&\n     +\n    -+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin >actual.out 2>actual.err &&\n    ++\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin 2>actual.err &&\n     +\tcat >expect <<-EOF &&\n     +\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n     +\tEOF\n    -+\ttest_must_be_empty actual.out &&\n     +\ttest_cmp expect actual.err\n     +'\n     +\n 4:  e599ea6d33 !  4:  2cc7318697 fetch: fix missing from-reference when fetching HEAD:foo\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    fetch: fix missing from-reference when fetching HEAD:foo\n    +    fetch: print left-hand side when fetching HEAD:foo\n     \n         `store_updated_refs()` parses the remote reference for two purposes:\n     \n    @@ Commit message\n         name and can thus be used for both cases. But if the remote reference is\n         HEAD, the parsed remote reference becomes empty. This is intended when\n         we write the FETCH_HEAD, where we skip writing the note in that case.\n    -    But it is not intended when displaying the updated references and would\n    -    cause us to miss the left-hand side of the displayed reference update:\n    +    But when displaying the updated references this leads to inconsistent\n    +    output where the left-hand side of reference updates is missing in some\n    +    cases:\n     \n         ```\n    -    $ git fetch origin HEAD:foo\n    -    From https://github.com/git/git\n    -     * [new ref]                          -> foo\n    -    ```\n    -\n    -    The HEAD string is clearly missing from the left-hand side of the arrow,\n    -    which is further stressed by the point that the following commands show\n    -    the left-hand side as expected:\n    -\n    -    ```\n    -    $ git fetch origin HEAD\n    +    $ git fetch origin HEAD HEAD:explicit-head :implicit-head main\n         From https://github.com/git/git\n          * branch                  HEAD       -> FETCH_HEAD\n    -\n    -    $ git fetch origin master\n    -    From https://github.com/git/git\n    -     * branch                  master     -> FETCH_HEAD\n    -     * branch                  master     -> origin/master\n    +     * [new ref]                          -> explicit-head\n    +     * [new ref]                          -> implicit-head\n    +     * branch                  main       -> FETCH_HEAD\n         ```\n     \n    +    This behaviour has existed ever since the table-based output has been\n    +    introduced for git-fetch(1) via 165f390250 (git-fetch: more terse fetch\n    +    output, 2007-11-03) and was never explicitly documented either in the\n    +    commit message or in any of our tests. So while it may not be a bug per\n    +    se, it feels like a weird inconsistency and not like it was a concious\n    +    design decision.\n    +\n         The logic of how we compute the remote reference name that we ultimately\n         pass to `display_ref_update()` is not easy to follow. There are three\n         different cases here:\n     \n             - When the remote reference name is \"HEAD\" we set the remote\n               reference name to the empty string. This is the case that causes\n    -          the bug to occur, where we would indeed want to print \"HEAD\"\n    -          instead of the empty string. This is what `prettify_refname()`\n    -          would return.\n    +          the left-hand side to go missing, where we would indeed want to\n    +          print \"HEAD\" instead of the empty string. This is what\n    +          `prettify_refname()` would return.\n     \n             - When the remote reference name has a well-known prefix then we\n               strip this prefix. This matches what `prettify_refname()` does.\n    @@ Commit message\n               matches what `prettify_refname()` does.\n     \n         As the return value of `prettify_refname()` would do the correct thing\n    -    for us in all three cases, we can fix the bug by passing through the\n    -    full remote reference name to `display_ref_update()`, which learns to\n    -    call `prettify_refname()`. At the same time, this also simplifies the\n    -    code a bit.\n    +    for us in all three cases, we can thus fix the inconsistency by passing\n    +    through the full remote reference name to `display_ref_update()`, which\n    +    learns to call `prettify_refname()`. At the same time, this also\n    +    simplifies the code a bit.\n     \n         Note that this patch also changes formatting of the block that computes\n    -    the \"kind\" and \"what\" variables. This is done on purpose so that it is\n    -    part of the diff, hopefully making the change easier to comprehend.\n    +    the \"kind\" (which is the category like \"branch\" or \"tag\") and \"what\"\n    +    (which is the prettified reference name like \"master\" or \"v1.0\")\n    +    variables. This is done on purpose so that it is part of the diff,\n    +    hopefully making the change easier to comprehend.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +\ttest_must_be_empty actual.out &&\n     +\ttest_cmp expect actual.err\n     +'\n    ++\n    ++test_expect_success 'fetch output with object ID' '\n    ++\ttest_when_finished \"rm -rf object-id\" &&\n    ++\tgit clone . object-id &&\n    ++\tcommit=$(git rev-parse HEAD) &&\n    ++\n    ++\tgit -C object-id fetch --dry-run origin $commit:object-id >actual.out 2>actual.err &&\n    ++\tcat >expect <<-EOF &&\n    ++\tFrom $(test-tool path-utils real_path .)/.\n    ++\t * [new ref]         $commit -> object-id\n    ++\tEOF\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err &&\n    ++\n    ++\tgit -C object-id fetch origin $commit:object-id >actual.out 2>actual.err &&\n    ++\ttest_must_be_empty actual.out &&\n    ++\ttest_cmp expect actual.err\n    ++'\n     +\n      test_expect_success '--no-show-forced-updates' '\n      \tmkdir forced-updates &&\n -:  ---------- >  5:  bb1a591c2f fetch: refactor calculation of the display table width\n 5:  80ac00b0c4 !  6:  3cac552f5f fetch: introduce `display_format` enum\n    @@ builtin/fetch.c: enum {\n      \n      \tchar *url;\n      \tint url_len, shown_url;\n    -@@ builtin/fetch.c: static int refcol_width(const struct ref *ref, int compact_format)\n    - static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n    - \t\t\t       const char *raw_url)\n    - {\n    --\tstruct ref *rm;\n    - \tconst char *format = \"full\";\n    - \tint i;\n    - \n     @@ builtin/fetch.c: static void display_state_init(struct display_state *display_state, struct ref *\n      \n      \tgit_config_get_string_tmp(\"fetch.output\", &format);\n    @@ builtin/fetch.c: static void display_state_init(struct display_state *display_st\n      \t\tdie(_(\"invalid value for '%s': '%s'\"),\n      \t\t    \"fetch.output\", format);\n      \n    --\tdisplay_state->refcol_width = 10;\n    --\tfor (rm = ref_map; rm; rm = rm->next) {\n    --\t\tint width;\n    +-\tdisplay_state->refcol_width = refcol_width(ref_map, display_state->compact_format);\n     +\tswitch (display_state->format) {\n     +\tcase DISPLAY_FORMAT_FULL:\n    -+\tcase DISPLAY_FORMAT_COMPACT: {\n    -+\t\tstruct ref *rm;\n    - \n    --\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n    --\t\t    !rm->peer_ref ||\n    --\t\t    !strcmp(rm->name, \"HEAD\"))\n    --\t\t\tcontinue;\n    -+\t\tdisplay_state->refcol_width = 10;\n    -+\t\tfor (rm = ref_map; rm; rm = rm->next) {\n    -+\t\t\tint width;\n    - \n    --\t\twidth = refcol_width(rm, display_state->compact_format);\n    -+\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n    -+\t\t\t    !rm->peer_ref ||\n    -+\t\t\t    !strcmp(rm->name, \"HEAD\"))\n    -+\t\t\t\tcontinue;\n    - \n    --\t\t/*\n    --\t\t * Not precise calculation for compact mode because '*' can\n    --\t\t * appear on the left hand side of '->' and shrink the column\n    --\t\t * back.\n    --\t\t */\n    --\t\tif (display_state->refcol_width < width)\n    --\t\t\tdisplay_state->refcol_width = width;\n    -+\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n    -+\n    -+\t\t\t/*\n    -+\t\t\t * Not precise calculation for compact mode because '*' can\n    -+\t\t\t * appear on the left hand side of '->' and shrink the column\n    -+\t\t\t * back.\n    -+\t\t\t */\n    -+\t\t\tif (display_state->refcol_width < width)\n    -+\t\t\t\tdisplay_state->refcol_width = width;\n    -+\t\t}\n    -+\n    ++\tcase DISPLAY_FORMAT_COMPACT:\n    ++\t\tdisplay_state->refcol_width = refcol_width(ref_map,\n    ++\t\t\t\t\t\t\t   display_state->format == DISPLAY_FORMAT_COMPACT);\n     +\t\tbreak;\n    -+\t}\n     +\tdefault:\n     +\t\tBUG(\"unexpected display format %d\", display_state->format);\n    - \t}\n    ++\t}\n      }\n      \n    + static void display_state_release(struct display_state *display_state)\n     @@ builtin/fetch.c: static void display_ref_update(struct display_state *display_state, char code,\n      \t\t\t       const char *remote, const char *local,\n      \t\t\t       int summary_width)\n 6:  826b8b7bc0 !  7:  0c3dbcd09f fetch: move display format parsing into main function\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    fetch: move display format parsing into main function\n    +    fetch: lift up parsing of \"fetch.output\" config variable\n     \n         Parsing the display format happens inside of `display_state_init()`. As\n         we only need to check for a simple config entry, this is a natural\n         location to put this code as it means that display-state logic is neatly\n         contained in a single location.\n     \n    -    We're about to introduce a output format though that is intended to be\n    -    parseable by machines, for example inside of a script. In that case it\n    -    becomes a bit awkward of an interface if you have to call git-fetch(1)\n    -    with the `fetch.output` config key set. We're thus going to introduce a\n    -    new `--output-format` switch for git-fetch(1) so that the output format\n    -    can be configured more directly.\n    -\n    -    This means we'll have to hook parsing of the display format into the\n    -    command line options parser. Having the code to determine the actual\n    -    output format scattered across two different sites is hard to reason\n    -    about though.\n    +    We're about to introduce a new \"porcelain\" output format though that is\n    +    intended to be parseable by machines, for example inside of a script.\n    +    This format can be enabled by passing the `--porcelain` switch to\n    +    git-fetch(1). As a consequence, we'll have to add a second callsite that\n    +    influences the output format, which will become awkward to handle.\n     \n         Refactor the code such that callers are expected to pass the display\n         format that is to be used into `display_state_init()`. This allows us to\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## builtin/fetch.c ##\n    -@@ builtin/fetch.c: static int refcol_width(const struct ref *ref, int compact_format)\n    +@@ builtin/fetch.c: static int fetch_write_commit_graph = -1;\n    + static int stdin_refspecs = 0;\n    + static int negotiate_only;\n    + \n    ++struct fetch_config {\n    ++\tenum display_format display_format;\n    ++};\n    ++\n    + static int git_fetch_config(const char *k, const char *v, void *cb)\n    + {\n    ++\tstruct fetch_config *fetch_config = cb;\n    ++\n    + \tif (!strcmp(k, \"fetch.prune\")) {\n    + \t\tfetch_prune_config = git_config_bool(k, v);\n    + \t\treturn 0;\n    +@@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v, void *cb)\n    + \t\treturn 0;\n    + \t}\n    + \n    ++\tif (!strcmp(k, \"fetch.output\")) {\n    ++\t\tif (!v)\n    ++\t\t\treturn config_error_nonbool(k);\n    ++\t\telse if (!strcasecmp(v, \"full\"))\n    ++\t\t\tfetch_config->display_format = DISPLAY_FORMAT_FULL;\n    ++\t\telse if (!strcasecmp(v, \"compact\"))\n    ++\t\t\tfetch_config->display_format = DISPLAY_FORMAT_COMPACT;\n    ++\t\telse\n    ++\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n    ++\t\t\t    \"fetch.output\", v);\n    ++\t}\n    ++\n    + \treturn git_default_config(k, v, cb);\n    + }\n    + \n    +@@ builtin/fetch.c: static int refcol_width(const struct ref *ref_map, int compact_format)\n      }\n      \n      static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n    @@ builtin/fetch.c: static void display_state_init(struct display_state *display_st\n     -\n      \tswitch (display_state->format) {\n      \tcase DISPLAY_FORMAT_FULL:\n    - \tcase DISPLAY_FORMAT_COMPACT: {\n    + \tcase DISPLAY_FORMAT_COMPACT:\n     @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,\n      }\n      \n    @@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const cha\n      \tsigchain_pop(SIGPIPE);\n      \trefspec_clear(&rs);\n      \ttransport_disconnect(gtransport);\n    -@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    +@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,\n    + \n    + int cmd_fetch(int argc, const char **argv, const char *prefix)\n      {\n    ++\tstruct fetch_config config = {\n    ++\t\t.display_format = DISPLAY_FORMAT_FULL,\n    ++\t};\n      \tint i;\n      \tconst char *bundle_uri;\n    -+\tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n      \tstruct string_list list = STRING_LIST_INIT_DUP;\n    - \tstruct remote *remote = NULL;\n    - \tint result = 0;\n     @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    - \targc = parse_options(argc, argv, prefix,\n    - \t\t\t     builtin_fetch_options, builtin_fetch_usage, 0);\n    - \n    -+\tif (display_format == DISPLAY_FORMAT_UNKNOWN) {\n    -+\t\tconst char *format = \"full\";\n    -+\n    -+\t\tgit_config_get_string_tmp(\"fetch.output\", &format);\n    -+\t\tif (!strcasecmp(format, \"full\"))\n    -+\t\t\tdisplay_format = DISPLAY_FORMAT_FULL;\n    -+\t\telse if (!strcasecmp(format, \"compact\"))\n    -+\t\t\tdisplay_format = DISPLAY_FORMAT_COMPACT;\n    -+\t\telse\n    -+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n    -+\t\t\t    \"fetch.output\", format);\n    -+\t}\n    -+\n    - \tif (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)\n    - \t\trecurse_submodules = recurse_submodules_cli;\n    + \t\tfree(anon);\n    + \t}\n      \n    +-\tgit_config(git_fetch_config, NULL);\n    ++\tgit_config(git_fetch_config, &config);\n    + \tif (the_repository->gitdir) {\n    + \t\tprepare_repo_settings(the_repository);\n    + \t\tthe_repository->settings.command_requires_full_index = 0;\n     @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \t} else if (remote) {\n      \t\tif (filter_options.choice || has_promisor_remote())\n      \t\t\tfetch_one_setup_partial(remote);\n     -\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n     +\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n    -+\t\t\t\t   display_format);\n    ++\t\t\t\t   config.display_format);\n      \t} else {\n      \t\tint max_children = max_jobs;\n      \n 7:  20f2e061d6 !  8:  8e33a08c35 fetch: move option related variables into main function\n    @@ builtin/fetch.c: static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n     -static int stdin_refspecs = 0;\n     -static int negotiate_only;\n      \n    - static int git_fetch_config(const char *k, const char *v, void *cb)\n    - {\n    + struct fetch_config {\n    + \tenum display_format display_format;\n     @@ builtin/fetch.c: static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n      \treturn 0;\n      }\n    @@ builtin/fetch.c: static int parse_refmap_arg(const struct option *opt, const cha\n      static void unlock_pack(unsigned int flags)\n      {\n      \tif (gtransport)\n    -@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,\n    - \n    - int cmd_fetch(int argc, const char **argv, const char *prefix)\n    - {\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \tstruct fetch_config config = {\n    + \t\t.display_format = DISPLAY_FORMAT_FULL,\n    + \t};\n     -\tint i;\n    - \tconst char *bundle_uri;\n     +\tconst char *submodule_prefix = \"\";\n    - \tenum display_format display_format = DISPLAY_FORMAT_UNKNOWN;\n    + \tconst char *bundle_uri;\n      \tstruct string_list list = STRING_LIST_INIT_DUP;\n      \tstruct remote *remote = NULL;\n     +\tint all = 0, multiple = 0;\n 8:  24ae381950 !  9:  d49152c220 fetch: introduce machine-parseable \"porcelain\" output format\n    @@ builtin/fetch.c: enum display_format {\n      \n      struct display_state {\n     @@ builtin/fetch.c: static void display_state_init(struct display_state *display_state, struct ref *\n    - \n    + \t\tdisplay_state->refcol_width = refcol_width(ref_map,\n    + \t\t\t\t\t\t\t   display_state->format == DISPLAY_FORMAT_COMPACT);\n      \t\tbreak;\n    - \t}\n     +\tcase DISPLAY_FORMAT_PORCELAIN:\n     +\t\t/* We don't need to precompute anything here. */\n     +\t\tbreak;\n    @@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_chi\n      \t\t\t\tprintf(_(\"Fetching %s\\n\"), name);\n      \t\t\tcmd.git_cmd = 1;\n      \t\t\tif (run_command(&cmd)) {\n    -@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,\n    - \treturn exit_code;\n    - }\n    +@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n    + \tint fetch_write_commit_graph = -1;\n    + \tint stdin_refspecs = 0;\n    + \tint negotiate_only = 0;\n    ++\tint porcelain = 0;\n    + \tint i;\n      \n    -+static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n    -+{\n    -+\tenum display_format *format = opt->value;\n    -+\t*format = DISPLAY_FORMAT_PORCELAIN;\n    -+\treturn 0;\n    -+}\n    -+\n    - int cmd_fetch(int argc, const char **argv, const char *prefix)\n    - {\n    - \tconst char *bundle_uri;\n    + \tstruct option builtin_fetch_options[] = {\n     @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n      \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n      \t\t\t N_(\"dry run\")),\n    -+\t\tOPT_CALLBACK_F(0, \"porcelain\", &display_format, NULL, N_(\"machine-readable output\"),\n    -+\t\t\t       PARSE_OPT_NOARG|PARSE_OPT_NONEG, opt_parse_porcelain),\n    ++\t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n      \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n      \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n      \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n    @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \t\tfetch_config_from_gitmodules(sfjc, rs);\n      \t}\n      \n    -+\tif (display_format == DISPLAY_FORMAT_PORCELAIN) {\n    ++\n    ++\tif (porcelain) {\n     +\t\tswitch (recurse_submodules_cli) {\n     +\t\tcase RECURSE_SUBMODULES_OFF:\n     +\t\tcase RECURSE_SUBMODULES_DEFAULT:\n    @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n     +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n     +\t\t\t    \"--porcelain\", \"--recurse-submodules\");\n     +\t\t}\n    ++\n    ++\t\tconfig.display_format = DISPLAY_FORMAT_PORCELAIN;\n     +\t}\n     +\n      \tif (negotiate_only && !negotiation_tip.nr)\n    @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \n      \t\t/* TODO should this also die if we have a previous partial-clone? */\n     -\t\tresult = fetch_multiple(&list, max_children);\n    -+\t\tresult = fetch_multiple(&list, max_children, display_format);\n    ++\t\tresult = fetch_multiple(&list, max_children, config.display_format);\n      \t}\n      \n    - \n    +-\n    + \t/*\n    + \t * This is only needed after fetch_one(), which does not fetch\n    + \t * submodules by itself.\n     @@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)\n      \t\tif (max_children < 0)\n      \t\t\tmax_children = fetch_parallel_config;\n      \n     -\t\tadd_options_to_argv(&options);\n    -+\t\tadd_options_to_argv(&options, display_format);\n    ++\t\tadd_options_to_argv(&options, config.display_format);\n      \t\tresult = fetch_submodules(the_repository,\n      \t\t\t\t\t  &options,\n      \t\t\t\t\t  submodule_prefix,\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch compact output' '\n     +\ttest_must_be_empty stderr &&\n     +\ttest_cmp expect stdout\n     +'\n    ++\n    ++test_expect_success 'fetch --no-porcelain overrides previous --porcelain' '\n    ++\ttest_when_finished \"rm -rf no-porcelain\" &&\n    ++\n    ++\tgit switch --create no-porcelain &&\n    ++\tgit clone . no-porcelain &&\n    ++\ttest_commit --no-tag no-porcelain &&\n    ++\told_commit=$(git rev-parse --short HEAD~) &&\n    ++\tnew_commit=$(git rev-parse --short HEAD) &&\n    ++\n    ++\tcat >expect <<-EOF &&\n    ++\tFrom $(test-tool path-utils real_path .)/.\n    ++\t   $old_commit..$new_commit  no-porcelain -> origin/no-porcelain\n    ++\tEOF\n    ++\n    ++\tgit -C no-porcelain fetch --porcelain --no-porcelain >stdout 2>stderr &&\n    ++\ttest_cmp expect stderr &&\n    ++\ttest_must_be_empty stdout\n    ++'\n     +\n      test_expect_success 'fetch output with HEAD' '\n      \ttest_when_finished \"rm -rf head\" &&\n    @@ t/t5574-fetch-output.sh: test_expect_success 'fetch output with HEAD' '\n     +\ttest_cmp expect actual\n     +'\n     +\n    - test_expect_success '--no-show-forced-updates' '\n    - \tmkdir forced-updates &&\n    - \t(\n    + test_expect_success 'fetch output with object ID' '\n    + \ttest_when_finished \"rm -rf object-id\" &&\n    + \tgit clone . object-id &&\n-- \n2.40.1\n\n"},{"id":"476959","messageId":"e459d8a1ec22f901e6c069d668981b1a1a0a0473.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 2/9] fetch: split out tests for output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:07Z","receivedAt":"2023-05-10T12:34:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new porcelain mode for the output of\ngit-fetch(1). As part of that we'll be introducing a set of new tests\nthat only relate to the output of this command.\n\nSplit out tests that exercise the output format of git-fetch(1) so that\nit becomes easier to verify this functionality as a standalone unit. As\nthe tests assume that the default branch is called \"main\" we set up the\ncorresponding GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME environment variable\naccordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5510-fetch.sh        | 53 ----------------------------------\n t/t5574-fetch-output.sh | 63 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 63 insertions(+), 53 deletions(-)\n create mode 100755 t/t5574-fetch-output.sh\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex dc44da9c79..4f289063ce 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1118,59 +1118,6 @@ test_expect_success 'fetching with auto-gc does not lock up' '\n \t)\n '\n \n-test_expect_success 'fetch aligned output' '\n-\tgit clone . full-output &&\n-\ttest_commit looooooooooooong-tag &&\n-\t(\n-\t\tcd full-output &&\n-\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain                 -> origin/main\n-\tlooooooooooooong-tag -> looooooooooooong-tag\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'fetch compact output' '\n-\tgit clone . compact &&\n-\ttest_commit extraaa &&\n-\t(\n-\t\tcd compact &&\n-\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n-\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n-\t) &&\n-\tcat >expect <<-\\EOF &&\n-\tmain       -> origin/*\n-\textraaa    -> *\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--no-show-forced-updates' '\n-\tmkdir forced-updates &&\n-\t(\n-\t\tcd forced-updates &&\n-\t\tgit init &&\n-\t\ttest_commit 1 &&\n-\t\ttest_commit 2\n-\t) &&\n-\tgit clone forced-updates forced-update-clone &&\n-\tgit clone forced-updates no-forced-update-clone &&\n-\tgit -C forced-updates reset --hard HEAD~1 &&\n-\t(\n-\t\tcd forced-update-clone &&\n-\t\tgit fetch --show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep \"(forced update)\" output\n-\t) &&\n-\t(\n-\t\tcd no-forced-update-clone &&\n-\t\tgit fetch --no-show-forced-updates origin 2>output &&\n-\t\ttest_i18ngrep ! \"(forced update)\" output\n-\t)\n-'\n-\n for section in fetch transfer\n do\n \ttest_expect_success \"$section.hideRefs affects connectivity check\" '\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nnew file mode 100755\nindex 0000000000..f91b654d38\n--- /dev/null\n+++ b/t/t5574-fetch-output.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='git fetch output format'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'fetch aligned output' '\n+\tgit clone . full-output &&\n+\ttest_commit looooooooooooong-tag &&\n+\t(\n+\t\tcd full-output &&\n+\t\tgit -c fetch.output=full fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain                 -> origin/main\n+\tlooooooooooooong-tag -> looooooooooooong-tag\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'fetch compact output' '\n+\tgit clone . compact &&\n+\ttest_commit extraaa &&\n+\t(\n+\t\tcd compact &&\n+\t\tgit -c fetch.output=compact fetch origin >actual 2>&1 &&\n+\t\tgrep -e \"->\" actual | cut -c 22- >../actual\n+\t) &&\n+\tcat >expect <<-\\EOF &&\n+\tmain       -> origin/*\n+\textraaa    -> *\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-show-forced-updates' '\n+\tmkdir forced-updates &&\n+\t(\n+\t\tcd forced-updates &&\n+\t\tgit init &&\n+\t\ttest_commit 1 &&\n+\t\ttest_commit 2\n+\t) &&\n+\tgit clone forced-updates forced-update-clone &&\n+\tgit clone forced-updates no-forced-update-clone &&\n+\tgit -C forced-updates reset --hard HEAD~1 &&\n+\t(\n+\t\tcd forced-update-clone &&\n+\t\tgit fetch --show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep \"(forced update)\" output\n+\t) &&\n+\t(\n+\t\tcd no-forced-update-clone &&\n+\t\tgit fetch --no-show-forced-updates origin 2>output &&\n+\t\ttest_i18ngrep ! \"(forced update)\" output\n+\t)\n+'\n+\n+test_done\n-- \n2.40.1\n\n"},{"id":"476960","messageId":"d503c425fe0298b2c8539834f2e3852dea096803.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 3/9] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:11Z","receivedAt":"2023-05-10T12:34:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Add a testcase that exercises the logic when an invalid output format is\npassed via the `fetch.output` configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t5574-fetch-output.sh | 24 ++++++++++++++++++++++++\n 1 file changed, 24 insertions(+)\n\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex f91b654d38..8a344e6790 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -7,6 +7,30 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+test_expect_success 'fetch with invalid output format configuration' '\n+\ttest_when_finished \"rm -rf clone\" &&\n+\tgit clone . clone &&\n+\n+\ttest_must_fail git -C clone -c fetch.output fetch origin 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\terror: missing value for ${SQ}fetch.output${SQ}\n+\tfatal: unable to parse ${SQ}fetch.output${SQ} from command-line config\n+\tEOF\n+\ttest_cmp expect actual.err &&\n+\n+\ttest_must_fail git -C clone -c fetch.output= fetch origin 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}${SQ}\n+\tEOF\n+\ttest_cmp expect actual.err &&\n+\n+\ttest_must_fail git -C clone -c fetch.output=garbage fetch origin 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tfatal: invalid value for ${SQ}fetch.output${SQ}: ${SQ}garbage${SQ}\n+\tEOF\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success 'fetch aligned output' '\n \tgit clone . full-output &&\n \ttest_commit looooooooooooong-tag &&\n-- \n2.40.1\n\n"},{"id":"476961","messageId":"2cc7318697ef6062f6deadf7a22feb26c2c829e8.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 4/9] fetch: print left-hand side when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:15Z","receivedAt":"2023-05-10T12:34:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"`store_updated_refs()` parses the remote reference for two purposes:\n\n    - It gets used as a note when writing FETCH_HEAD.\n\n    - It is passed through to `display_ref_update()` to display\n      updated references in the following format:\n\n      ```\n       * branch               master          -> master\n      ```\n\nIn most cases, the parsed remote reference is the prettified reference\nname and can thus be used for both cases. But if the remote reference is\nHEAD, the parsed remote reference becomes empty. This is intended when\nwe write the FETCH_HEAD, where we skip writing the note in that case.\nBut when displaying the updated references this leads to inconsistent\noutput where the left-hand side of reference updates is missing in some\ncases:\n\n```\n$ git fetch origin HEAD HEAD:explicit-head :implicit-head main\nFrom https://github.com/git/git\n * branch                  HEAD       -> FETCH_HEAD\n * [new ref]                          -> explicit-head\n * [new ref]                          -> implicit-head\n * branch                  main       -> FETCH_HEAD\n```\n\nThis behaviour has existed ever since the table-based output has been\nintroduced for git-fetch(1) via 165f390250 (git-fetch: more terse fetch\noutput, 2007-11-03) and was never explicitly documented either in the\ncommit message or in any of our tests. So while it may not be a bug per\nse, it feels like a weird inconsistency and not like it was a concious\ndesign decision.\n\nThe logic of how we compute the remote reference name that we ultimately\npass to `display_ref_update()` is not easy to follow. There are three\ndifferent cases here:\n\n    - When the remote reference name is \"HEAD\" we set the remote\n      reference name to the empty string. This is the case that causes\n      the left-hand side to go missing, where we would indeed want to\n      print \"HEAD\" instead of the empty string. This is what\n      `prettify_refname()` would return.\n\n    - When the remote reference name has a well-known prefix then we\n      strip this prefix. This matches what `prettify_refname()` does.\n\n    - Otherwise, we keep the fully qualified reference name. This also\n      matches what `prettify_refname()` does.\n\nAs the return value of `prettify_refname()` would do the correct thing\nfor us in all three cases, we can thus fix the inconsistency by passing\nthrough the full remote reference name to `display_ref_update()`, which\nlearns to call `prettify_refname()`. At the same time, this also\nsimplifies the code a bit.\n\nNote that this patch also changes formatting of the block that computes\nthe \"kind\" (which is the category like \"branch\" or \"tag\") and \"what\"\n(which is the prettified reference name like \"master\" or \"v1.0\")\nvariables. This is done on purpose so that it is part of the diff,\nhopefully making the change easier to comprehend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c         | 37 ++++++++++++++++----------------\n t/t5574-fetch-output.sh | 47 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 66 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 08d7fc7233..6aecf549e8 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -918,12 +918,14 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t}\n \n \twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\tremote = prettify_refname(remote);\n+\tlocal = prettify_refname(local);\n \n \tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n \tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, prettify_refname(local));\n+\t\tprint_remote_to_local(display_state, remote, local);\n \telse\n-\t\tprint_compact(display_state, remote, prettify_refname(local));\n+\t\tprint_compact(display_state, remote, local);\n \tif (error)\n \t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n \tstrbuf_addch(&display_state->buf, '\\n');\n@@ -934,7 +936,7 @@ static void display_ref_update(struct display_state *display_state, char code,\n static int update_local_ref(struct ref *ref,\n \t\t\t    struct ref_transaction *transaction,\n \t\t\t    struct display_state *display_state,\n-\t\t\t    const char *remote, const struct ref *remote_ref,\n+\t\t\t    const struct ref *remote_ref,\n \t\t\t    int summary_width)\n {\n \tstruct commit *current = NULL, *updated;\n@@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -959,7 +961,7 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -970,12 +972,12 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -1008,7 +1010,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn r;\n \t}\n \n@@ -1030,7 +1032,7 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -1042,12 +1044,12 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1252,14 +1254,13 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\tif (!strcmp(rm->name, \"HEAD\")) {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = \"\";\n-\t\t\t}\n-\t\t\telse if (skip_prefix(rm->name, \"refs/heads/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/heads/\", &what)) {\n \t\t\t\tkind = \"branch\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/tags/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/tags/\", &what)) {\n \t\t\t\tkind = \"tag\";\n-\t\t\telse if (skip_prefix(rm->name, \"refs/remotes/\", &what))\n+\t\t\t} else if (skip_prefix(rm->name, \"refs/remotes/\", &what)) {\n \t\t\t\tkind = \"remote-tracking branch\";\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tkind = \"\";\n \t\t\t\twhat = rm->name;\n \t\t\t}\n@@ -1277,7 +1278,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t\t  display_state->url_len);\n \n \t\t\tif (ref) {\n-\t\t\t\trc |= update_local_ref(ref, transaction, display_state, what,\n+\t\t\t\trc |= update_local_ref(ref, transaction, display_state,\n \t\t\t\t\t\t       rm, summary_width);\n \t\t\t\tfree(ref);\n \t\t\t} else if (write_fetch_head || dry_run) {\n@@ -1288,7 +1289,7 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\t */\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n-\t\t\t\t\t\t   *what ? what : \"HEAD\",\n+\t\t\t\t\t\t   rm->name,\n \t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n \t\t\t}\n \t\t}\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 8a344e6790..9890f6f381 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -61,6 +61,53 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\n+\tgit -C head fetch --dry-run origin HEAD >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * branch            HEAD       -> FETCH_HEAD\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch --dry-run origin HEAD:foo >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         HEAD       -> foo\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C head fetch origin HEAD:foo >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n+test_expect_success 'fetch output with object ID' '\n+\ttest_when_finished \"rm -rf object-id\" &&\n+\tgit clone . object-id &&\n+\tcommit=$(git rev-parse HEAD) &&\n+\n+\tgit -C object-id fetch --dry-run origin $commit:object-id >actual.out 2>actual.err &&\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t * [new ref]         $commit -> object-id\n+\tEOF\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err &&\n+\n+\tgit -C object-id fetch origin $commit:object-id >actual.out 2>actual.err &&\n+\ttest_must_be_empty actual.out &&\n+\ttest_cmp expect actual.err\n+'\n+\n test_expect_success '--no-show-forced-updates' '\n \tmkdir forced-updates &&\n \t(\n-- \n2.40.1\n\n"},{"id":"476962","messageId":"bb1a591c2f46b78eb26eb72ec677c65d10cb714c.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 5/9] fetch: refactor calculation of the display table width","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:20Z","receivedAt":"2023-05-10T12:34:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When displaying reference updates, we try to print the references in a\nneat table. As the table's width is determined its contents we thus need\nto precalculate the overall width before we can start printing updated\nreferences.\n\nThe calculation is driven by `display_state_init()`, which invokes\n`refcol_width()` for every reference that is to be printed. This split\nis somewhat confusing. For one, we filter references that shall be\nattributed to the overall width in both places. And second, we\nneedlessly recalculate the maximum line length based on the terminal\ncolumns and display format for every reference.\n\nRefactor the code so that the complete width calculations are neatly\ncontained in `refcol_width()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 81 ++++++++++++++++++++++---------------------------\n 1 file changed, 37 insertions(+), 44 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 6aecf549e8..007eb3693d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -751,40 +751,51 @@ static int s_update_ref(const char *action,\n \treturn ret;\n }\n \n-static int refcol_width(const struct ref *ref, int compact_format)\n+static int refcol_width(const struct ref *ref_map, int compact_format)\n {\n-\tint max, rlen, llen, len;\n+\tconst struct ref *ref;\n+\tint max, width = 10;\n \n-\t/* uptodate lines are only shown on high verbosity level */\n-\tif (verbosity <= 0 && oideq(&ref->peer_ref->old_oid, &ref->old_oid))\n-\t\treturn 0;\n-\n-\tmax    = term_columns();\n-\trlen   = utf8_strwidth(prettify_refname(ref->name));\n-\n-\tllen   = utf8_strwidth(prettify_refname(ref->peer_ref->name));\n-\n-\t/*\n-\t * rough estimation to see if the output line is too long and\n-\t * should not be counted (we can't do precise calculation\n-\t * anyway because we don't know if the error explanation part\n-\t * will be printed in update_local_ref)\n-\t */\n-\tif (compact_format) {\n-\t\tllen = 0;\n+\tmax = term_columns();\n+\tif (compact_format)\n \t\tmax = max * 2 / 3;\n-\t}\n-\tlen = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen;\n-\tif (len >= max)\n-\t\treturn 0;\n \n-\treturn rlen;\n+\tfor (ref = ref_map; ref; ref = ref->next) {\n+\t\tint rlen, llen = 0, len;\n+\n+\t\tif (ref->status == REF_STATUS_REJECT_SHALLOW ||\n+\t\t    !ref->peer_ref ||\n+\t\t    !strcmp(ref->name, \"HEAD\"))\n+\t\t\tcontinue;\n+\n+\t\t/* uptodate lines are only shown on high verbosity level */\n+\t\tif (verbosity <= 0 && oideq(&ref->peer_ref->old_oid, &ref->old_oid))\n+\t\t\tcontinue;\n+\n+\t\trlen = utf8_strwidth(prettify_refname(ref->name));\n+\t\tif (!compact_format)\n+\t\t\tllen = utf8_strwidth(prettify_refname(ref->peer_ref->name));\n+\n+\t\t/*\n+\t\t * rough estimation to see if the output line is too long and\n+\t\t * should not be counted (we can't do precise calculation\n+\t\t * anyway because we don't know if the error explanation part\n+\t\t * will be printed in update_local_ref)\n+\t\t */\n+\t\tlen = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen;\n+\t\tif (len >= max)\n+\t\t\tcontinue;\n+\n+\t\tif (width < rlen)\n+\t\t\twidth = rlen;\n+\t}\n+\n+\treturn width;\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n \t\t\t       const char *raw_url)\n {\n-\tstruct ref *rm;\n \tconst char *format = \"full\";\n \tint i;\n \n@@ -816,25 +827,7 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = 10;\n-\tfor (rm = ref_map; rm; rm = rm->next) {\n-\t\tint width;\n-\n-\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n-\t\t    !rm->peer_ref ||\n-\t\t    !strcmp(rm->name, \"HEAD\"))\n-\t\t\tcontinue;\n-\n-\t\twidth = refcol_width(rm, display_state->compact_format);\n-\n-\t\t/*\n-\t\t * Not precise calculation for compact mode because '*' can\n-\t\t * appear on the left hand side of '->' and shrink the column\n-\t\t * back.\n-\t\t */\n-\t\tif (display_state->refcol_width < width)\n-\t\t\tdisplay_state->refcol_width = width;\n-\t}\n+\tdisplay_state->refcol_width = refcol_width(ref_map, display_state->compact_format);\n }\n \n static void display_state_release(struct display_state *display_state)\n-- \n2.40.1\n\n"},{"id":"476963","messageId":"3cac552f5ff85f84f986de02bbafa7e4ea0be848.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 6/9] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:24Z","receivedAt":"2023-05-10T12:34:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We currently have two different display formats in git-fetch(1) with the\n\"full\" and \"compact\" formats. This is tracked with a boolean value that\nsimply denotes whether the display format is supposed to be compacted\nor not. This works reasonably well while there are only two formats, but\nwe're about to introduce another format that will make this a bit more\nawkward to use.\n\nIntroduce a `enum display_format` that is more readily extensible.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 69 +++++++++++++++++++++++++++++++++----------------\n 1 file changed, 47 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 007eb3693d..4c58ee503a 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -48,11 +48,17 @@ enum {\n \tTAGS_SET = 2\n };\n \n+enum display_format {\n+\tDISPLAY_FORMAT_UNKNOWN = 0,\n+\tDISPLAY_FORMAT_FULL,\n+\tDISPLAY_FORMAT_COMPACT,\n+};\n+\n struct display_state {\n \tstruct strbuf buf;\n \n \tint refcol_width;\n-\tint compact_format;\n+\tenum display_format format;\n \n \tchar *url;\n \tint url_len, shown_url;\n@@ -820,14 +826,22 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \n \tgit_config_get_string_tmp(\"fetch.output\", &format);\n \tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->compact_format = 0;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n \telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->compact_format = 1;\n+\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t    \"fetch.output\", format);\n \n-\tdisplay_state->refcol_width = refcol_width(ref_map, display_state->compact_format);\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT:\n+\t\tdisplay_state->refcol_width = refcol_width(ref_map,\n+\t\t\t\t\t\t\t   display_state->format == DISPLAY_FORMAT_COMPACT);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t}\n }\n \n static void display_state_release(struct display_state *display_state)\n@@ -897,30 +911,41 @@ static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *remote, const char *local,\n \t\t\t       int summary_width)\n {\n-\tint width;\n-\n \tif (verbosity < 0)\n \t\treturn;\n \n \tstrbuf_reset(&display_state->buf);\n \n-\tif (!display_state->shown_url) {\n-\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n-\t\t\t    display_state->url_len, display_state->url);\n-\t\tdisplay_state->shown_url = 1;\n+\tswitch (display_state->format) {\n+\tcase DISPLAY_FORMAT_FULL:\n+\tcase DISPLAY_FORMAT_COMPACT: {\n+\t\tint width;\n+\n+\t\tif (!display_state->shown_url) {\n+\t\t\tstrbuf_addf(&display_state->buf, _(\"From %.*s\\n\"),\n+\t\t\t\t    display_state->url_len, display_state->url);\n+\t\t\tdisplay_state->shown_url = 1;\n+\t\t}\n+\n+\t\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n+\t\tremote = prettify_refname(remote);\n+\t\tlocal = prettify_refname(local);\n+\n+\t\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n+\n+\t\tif (display_state->format != DISPLAY_FORMAT_COMPACT)\n+\t\t\tprint_remote_to_local(display_state, remote, local);\n+\t\telse\n+\t\t\tprint_compact(display_state, remote, local);\n+\n+\t\tif (error)\n+\t\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\n+\t\tbreak;\n \t}\n-\n-\twidth = (summary_width + strlen(summary) - gettext_width(summary));\n-\tremote = prettify_refname(remote);\n-\tlocal = prettify_refname(local);\n-\n-\tstrbuf_addf(&display_state->buf, \" %c %-*s \", code, width, summary);\n-\tif (!display_state->compact_format)\n-\t\tprint_remote_to_local(display_state, remote, local);\n-\telse\n-\t\tprint_compact(display_state, remote, local);\n-\tif (error)\n-\t\tstrbuf_addf(&display_state->buf, \"  (%s)\", error);\n+\tdefault:\n+\t\tBUG(\"unexpected display format %d\", display_state->format);\n+\t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n \tfputs(display_state->buf.buf, stderr);\n-- \n2.40.1\n\n"},{"id":"476964","messageId":"0c3dbcd09fb5e064601dc3fd9ac650cb496371e5.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 7/9] fetch: lift up parsing of \"fetch.output\" config variable","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:28Z","receivedAt":"2023-05-10T12:34:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Parsing the display format happens inside of `display_state_init()`. As\nwe only need to check for a simple config entry, this is a natural\nlocation to put this code as it means that display-state logic is neatly\ncontained in a single location.\n\nWe're about to introduce a new \"porcelain\" output format though that is\nintended to be parseable by machines, for example inside of a script.\nThis format can be enabled by passing the `--porcelain` switch to\ngit-fetch(1). As a consequence, we'll have to add a second callsite that\ninfluences the output format, which will become awkward to handle.\n\nRefactor the code such that callers are expected to pass the display\nformat that is to be used into `display_state_init()`. This allows us to\nlift up the code into the main function, where we can then hook it into\ncommand line options parser in a follow-up commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 50 +++++++++++++++++++++++++++++++------------------\n 1 file changed, 32 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 4c58ee503a..439d3c667b 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -104,8 +104,14 @@ static int fetch_write_commit_graph = -1;\n static int stdin_refspecs = 0;\n static int negotiate_only;\n \n+struct fetch_config {\n+\tenum display_format display_format;\n+};\n+\n static int git_fetch_config(const char *k, const char *v, void *cb)\n {\n+\tstruct fetch_config *fetch_config = cb;\n+\n \tif (!strcmp(k, \"fetch.prune\")) {\n \t\tfetch_prune_config = git_config_bool(k, v);\n \t\treturn 0;\n@@ -144,6 +150,18 @@ static int git_fetch_config(const char *k, const char *v, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(k, \"fetch.output\")) {\n+\t\tif (!v)\n+\t\t\treturn config_error_nonbool(k);\n+\t\telse if (!strcasecmp(v, \"full\"))\n+\t\t\tfetch_config->display_format = DISPLAY_FORMAT_FULL;\n+\t\telse if (!strcasecmp(v, \"compact\"))\n+\t\t\tfetch_config->display_format = DISPLAY_FORMAT_COMPACT;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t    \"fetch.output\", v);\n+\t}\n+\n \treturn git_default_config(k, v, cb);\n }\n \n@@ -800,14 +818,13 @@ static int refcol_width(const struct ref *ref_map, int compact_format)\n }\n \n static void display_state_init(struct display_state *display_state, struct ref *ref_map,\n-\t\t\t       const char *raw_url)\n+\t\t\t       const char *raw_url, enum display_format format)\n {\n-\tconst char *format = \"full\";\n \tint i;\n \n \tmemset(display_state, 0, sizeof(*display_state));\n-\n \tstrbuf_init(&display_state->buf, 0);\n+\tdisplay_state->format = format;\n \n \tif (raw_url)\n \t\tdisplay_state->url = transport_anonymize_url(raw_url);\n@@ -824,15 +841,6 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \tif (verbosity < 0)\n \t\treturn;\n \n-\tgit_config_get_string_tmp(\"fetch.output\", &format);\n-\tif (!strcasecmp(format, \"full\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_FULL;\n-\telse if (!strcasecmp(format, \"compact\"))\n-\t\tdisplay_state->format = DISPLAY_FORMAT_COMPACT;\n-\telse\n-\t\tdie(_(\"invalid value for '%s': '%s'\"),\n-\t\t    \"fetch.output\", format);\n-\n \tswitch (display_state->format) {\n \tcase DISPLAY_FORMAT_FULL:\n \tcase DISPLAY_FORMAT_COMPACT:\n@@ -1605,7 +1613,8 @@ static int backfill_tags(struct display_state *display_state,\n }\n \n static int do_fetch(struct transport *transport,\n-\t\t    struct refspec *rs)\n+\t\t    struct refspec *rs,\n+\t\t    enum display_format display_format)\n {\n \tstruct ref_transaction *transaction = NULL;\n \tstruct ref *ref_map = NULL;\n@@ -1691,7 +1700,7 @@ static int do_fetch(struct transport *transport,\n \tif (retcode)\n \t\tgoto cleanup;\n \n-\tdisplay_state_init(&display_state, ref_map, transport->url);\n+\tdisplay_state_init(&display_state, ref_map, transport->url, display_format);\n \n \tif (atomic_fetch) {\n \t\ttransaction = ref_transaction_begin(&err);\n@@ -2069,7 +2078,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n-\t\t     int prune_tags_ok, int use_stdin_refspecs)\n+\t\t     int prune_tags_ok, int use_stdin_refspecs,\n+\t\t     enum display_format display_format)\n {\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tint i;\n@@ -2136,7 +2146,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \tsigchain_push_common(unlock_pack_on_signal);\n \tatexit(unlock_pack_atexit);\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\texit_code = do_fetch(gtransport, &rs);\n+\texit_code = do_fetch(gtransport, &rs, display_format);\n \tsigchain_pop(SIGPIPE);\n \trefspec_clear(&rs);\n \ttransport_disconnect(gtransport);\n@@ -2146,6 +2156,9 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,\n \n int cmd_fetch(int argc, const char **argv, const char *prefix)\n {\n+\tstruct fetch_config config = {\n+\t\t.display_format = DISPLAY_FORMAT_FULL,\n+\t};\n \tint i;\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -2165,7 +2178,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tfree(anon);\n \t}\n \n-\tgit_config(git_fetch_config, NULL);\n+\tgit_config(git_fetch_config, &config);\n \tif (the_repository->gitdir) {\n \t\tprepare_repo_settings(the_repository);\n \t\tthe_repository->settings.command_requires_full_index = 0;\n@@ -2302,7 +2315,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t} else if (remote) {\n \t\tif (filter_options.choice || has_promisor_remote())\n \t\t\tfetch_one_setup_partial(remote);\n-\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);\n+\t\tresult = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,\n+\t\t\t\t   config.display_format);\n \t} else {\n \t\tint max_children = max_jobs;\n \n-- \n2.40.1\n\n"},{"id":"476965","messageId":"8e33a08c35fd083903bfaf1e90634e690981cd4d.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 8/9] fetch: move option related variables into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:32Z","receivedAt":"2023-05-10T12:35:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The options of git-fetch(1) which we pass to `parse_options()` are\ndeclared globally in `builtin/fetch.c`. This means we're forced to use\nglobal variables for all the options, which is more likely to cause\nconfusion than explicitly passing state around.\n\nRefactor the code to move the options into `cmd_fetch()`. Move variables\nthat were previously forced to be declared globally and which are only\nused by `cmd_fetch()` into function-local scope.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fetch.c | 197 ++++++++++++++++++++++++------------------------\n 1 file changed, 100 insertions(+), 97 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 439d3c667b..7a3d620c4c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -75,13 +75,12 @@ static int fetch_prune_tags_config = -1; /* unspecified */\n static int prune_tags = -1; /* unspecified */\n #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */\n \n-static int all, append, dry_run, force, keep, multiple, update_head_ok;\n+static int append, dry_run, force, keep, update_head_ok;\n static int write_fetch_head = 1;\n static int verbosity, deepen_relative, set_upstream, refetch;\n static int progress = -1;\n-static int enable_auto_gc = 1;\n-static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;\n-static int max_jobs = -1, submodule_fetch_jobs_config = -1;\n+static int tags = TAGS_DEFAULT, update_shallow, deepen;\n+static int submodule_fetch_jobs_config = -1;\n static int fetch_parallel_config = 1;\n static int atomic_fetch;\n static enum transport_family family;\n@@ -92,17 +91,11 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;\n static struct strbuf default_rla = STRBUF_INIT;\n static struct transport *gtransport;\n static struct transport *gsecondary;\n-static const char *submodule_prefix = \"\";\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n-static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n static struct refspec refmap = REFSPEC_INIT_FETCH;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n static struct string_list server_options = STRING_LIST_INIT_DUP;\n static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;\n-static int fetch_write_commit_graph = -1;\n-static int stdin_refspecs = 0;\n-static int negotiate_only;\n \n struct fetch_config {\n \tenum display_format display_format;\n@@ -178,92 +171,6 @@ static int parse_refmap_arg(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-static struct option builtin_fetch_options[] = {\n-\tOPT__VERBOSITY(&verbosity),\n-\tOPT_BOOL(0, \"all\", &all,\n-\t\t N_(\"fetch from all remotes\")),\n-\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n-\t\t N_(\"set upstream for git pull/fetch\")),\n-\tOPT_BOOL('a', \"append\", &append,\n-\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n-\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n-\t\t N_(\"use atomic transaction to update references\")),\n-\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to upload pack on remote end\")),\n-\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n-\tOPT_BOOL('m', \"multiple\", &multiple,\n-\t\t N_(\"fetch from multiple remotes\")),\n-\tOPT_SET_INT('t', \"tags\", &tags,\n-\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n-\tOPT_SET_INT('n', NULL, &tags,\n-\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules fetched in parallel\")),\n-\tOPT_BOOL(0, \"prefetch\", &prefetch,\n-\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n-\tOPT_BOOL('p', \"prune\", &prune,\n-\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n-\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n-\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n-\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n-\t\t    N_(\"control recursive fetching of submodules\"),\n-\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"dry-run\", &dry_run,\n-\t\t N_(\"dry run\")),\n-\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n-\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n-\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n-\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n-\t\t    N_(\"allow updating of HEAD ref\")),\n-\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n-\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n-\t\t   N_(\"deepen history of shallow clone\")),\n-\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n-\t\t   N_(\"deepen history of shallow repository based on time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n-\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n-\t\t    N_(\"deepen history of shallow clone\")),\n-\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n-\t\t      N_(\"convert to a complete repository\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n-\t\t      N_(\"re-fetch without negotiating common commits\"),\n-\t\t      1, PARSE_OPT_NONEG),\n-\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n-\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n-\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n-\t\t   N_(\"default for recursive fetching of submodules \"\n-\t\t      \"(lower priority than config files)\"),\n-\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n-\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n-\t\t N_(\"accept refs that update .git/shallow\")),\n-\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n-\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n-\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV4),\n-\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n-\t\t\tTRANSPORT_FAMILY_IPV6),\n-\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n-\t\t\tN_(\"report that we have only objects reachable from this object\")),\n-\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n-\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n-\t\t N_(\"run 'maintenance --auto' after fetching\")),\n-\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n-\t\t N_(\"check for forced-updates on all updated branches\")),\n-\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n-\t\t N_(\"write the commit-graph after fetching\")),\n-\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n-\t\t N_(\"accept refspecs from stdin\")),\n-\tOPT_END()\n-};\n-\n static void unlock_pack(unsigned int flags)\n {\n \tif (gtransport)\n@@ -2159,12 +2066,108 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tstruct fetch_config config = {\n \t\t.display_format = DISPLAY_FORMAT_FULL,\n \t};\n-\tint i;\n+\tconst char *submodule_prefix = \"\";\n \tconst char *bundle_uri;\n \tstruct string_list list = STRING_LIST_INIT_DUP;\n \tstruct remote *remote = NULL;\n+\tint all = 0, multiple = 0;\n \tint result = 0;\n \tint prune_tags_ok = 1;\n+\tint enable_auto_gc = 1;\n+\tint unshallow = 0;\n+\tint max_jobs = -1;\n+\tint recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;\n+\tint recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;\n+\tint fetch_write_commit_graph = -1;\n+\tint stdin_refspecs = 0;\n+\tint negotiate_only = 0;\n+\tint i;\n+\n+\tstruct option builtin_fetch_options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_BOOL(0, \"all\", &all,\n+\t\t\t N_(\"fetch from all remotes\")),\n+\t\tOPT_BOOL(0, \"set-upstream\", &set_upstream,\n+\t\t\t N_(\"set upstream for git pull/fetch\")),\n+\t\tOPT_BOOL('a', \"append\", &append,\n+\t\t\t N_(\"append to .git/FETCH_HEAD instead of overwriting\")),\n+\t\tOPT_BOOL(0, \"atomic\", &atomic_fetch,\n+\t\t\t N_(\"use atomic transaction to update references\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to upload pack on remote end\")),\n+\t\tOPT__FORCE(&force, N_(\"force overwrite of local reference\"), 0),\n+\t\tOPT_BOOL('m', \"multiple\", &multiple,\n+\t\t\t N_(\"fetch from multiple remotes\")),\n+\t\tOPT_SET_INT('t', \"tags\", &tags,\n+\t\t\t    N_(\"fetch all tags and associated objects\"), TAGS_SET),\n+\t\tOPT_SET_INT('n', NULL, &tags,\n+\t\t\t    N_(\"do not fetch all tags (--no-tags)\"), TAGS_UNSET),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules fetched in parallel\")),\n+\t\tOPT_BOOL(0, \"prefetch\", &prefetch,\n+\t\t\t N_(\"modify the refspec to place all refs within refs/prefetch/\")),\n+\t\tOPT_BOOL('p', \"prune\", &prune,\n+\t\t\t N_(\"prune remote-tracking branches no longer on remote\")),\n+\t\tOPT_BOOL('P', \"prune-tags\", &prune_tags,\n+\t\t\t N_(\"prune local tags no longer on remote and clobber changed tags\")),\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules\", &recurse_submodules_cli, N_(\"on-demand\"),\n+\t\t\t    N_(\"control recursive fetching of submodules\"),\n+\t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n+\t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n+\t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n+\t\tOPT_BOOL('u', \"update-head-ok\", &update_head_ok,\n+\t\t\t    N_(\"allow updating of HEAD ref\")),\n+\t\tOPT_BOOL(0, \"progress\", &progress, N_(\"force progress reporting\")),\n+\t\tOPT_STRING(0, \"depth\", &depth, N_(\"depth\"),\n+\t\t\t   N_(\"deepen history of shallow clone\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &deepen_since, N_(\"time\"),\n+\t\t\t   N_(\"deepen history of shallow repository based on time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &deepen_not, N_(\"revision\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding rev\")),\n+\t\tOPT_INTEGER(0, \"deepen\", &deepen_relative,\n+\t\t\t    N_(\"deepen history of shallow clone\")),\n+\t\tOPT_SET_INT_F(0, \"unshallow\", &unshallow,\n+\t\t\t      N_(\"convert to a complete repository\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n+\t\t\t      N_(\"re-fetch without negotiating common commits\"),\n+\t\t\t      1, PARSE_OPT_NONEG),\n+\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n+\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n+\t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n+\t\t\t   N_(\"default for recursive fetching of submodules \"\n+\t\t\t      \"(lower priority than config files)\"),\n+\t\t\t   PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules),\n+\t\tOPT_BOOL(0, \"update-shallow\", &update_shallow,\n+\t\t\t N_(\"accept refs that update .git/shallow\")),\n+\t\tOPT_CALLBACK_F(0, \"refmap\", NULL, N_(\"refmap\"),\n+\t\t\t       N_(\"specify fetch refmap\"), PARSE_OPT_NONEG, parse_refmap_arg),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options, N_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_SET_INT('4', \"ipv4\", &family, N_(\"use IPv4 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV4),\n+\t\tOPT_SET_INT('6', \"ipv6\", &family, N_(\"use IPv6 addresses only\"),\n+\t\t\t\tTRANSPORT_FAMILY_IPV6),\n+\t\tOPT_STRING_LIST(0, \"negotiation-tip\", &negotiation_tip, N_(\"revision\"),\n+\t\t\t\tN_(\"report that we have only objects reachable from this object\")),\n+\t\tOPT_BOOL(0, \"negotiate-only\", &negotiate_only,\n+\t\t\t N_(\"do not fetch a packfile; instead, print ancestors of negotiation tips\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"auto-maintenance\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"auto-gc\", &enable_auto_gc,\n+\t\t\t N_(\"run 'maintenance --auto' after fetching\")),\n+\t\tOPT_BOOL(0, \"show-forced-updates\", &fetch_show_forced_updates,\n+\t\t\t N_(\"check for forced-updates on all updated branches\")),\n+\t\tOPT_BOOL(0, \"write-commit-graph\", &fetch_write_commit_graph,\n+\t\t\t N_(\"write the commit-graph after fetching\")),\n+\t\tOPT_BOOL(0, \"stdin\", &stdin_refspecs,\n+\t\t\t N_(\"accept refspecs from stdin\")),\n+\t\tOPT_END()\n+\t};\n \n \tpacket_trace_identity(\"fetch\");\n \n-- \n2.40.1\n\n"},{"id":"476966","messageId":"d49152c22068d18cf9f8a2f1e5fe92a3b1f13e37.1683721293.git.ps@pks.im","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"[PATCH v5 9/9] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:36Z","receivedAt":"2023-05-10T12:35:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The output of git-fetch(1) is obviously designed for consumption by\nusers, only: we neatly columnize data, we abbreviate reference names, we\nprint neat arrows and we don't provide information about actual object\nIDs that have changed. This makes the output format basically unusable\nin the context of scripted invocations of git-fetch(1) that want to\nlearn about the exact changes that the command performs.\n\nIntroduce a new machine-parseable \"porcelain\" output format that is\nsupposed to fix this shortcoming. This output format is intended to\nprovide information about every reference that is about to be updated,\nthe old object ID that the reference has been pointing to and the new\nobject ID it will be updated to. Furthermore, the output format provides\nthe same flags as the human-readable format to indicate basic conditions\nfor each reference update like whether it was a fast-forward update, a\nbranch deletion, a rejected update or others.\n\nThe output format is quite simple:\n\n```\n<flag> <old-object-id> <new-object-id> <local-reference>\\n\n```\n\nWe assume two conditions which are generally true:\n\n    - The old and new object IDs have fixed known widths and cannot\n      contain spaces.\n\n    - References cannot contain newlines.\n\nWith these assumptions, the output format becomes unambiguously\nparseable. Furthermore, given that this output is designed to be\nconsumed by scripts, the machine-readable data is printed to stdout\ninstead of stderr like the human-readable output is. This is mostly done\nso that other data printed to stderr, like error messages or progress\nmeters, don't interfere with the parseable data.\n\nA notable ommission here is that the output format does not include the\nremote from which a reference was fetched, which might be important\ninformation especially in the context of multi-remote fetches. But as\nsuch a format would require us to print the remote for every single\nreference update due to parallelizable fetches it feels wasteful for the\nmost likely usecase, which is when fetching from a single remote.\n\nIn a similar spirit, a second restriction is that this cannot be used\nwith `--recurse-submodules`. This is because any reference updates would\nbe ambiguous without also printing the repository in which the update\nhappens.\n\nConsidering that both multi-remote and submodule fetches are user-facing\nfeatures, using them in conjunction with `--porcelain` that is intended\nfor scripting purposes is likely not going to be useful in the majority\nof cases. With that in mind these restrictions feel acceptable. If\nusecases for either of these come up in the future though it is easy\nenough to add a new \"porcelain-v2\" format that adds this information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/fetch-options.txt |   7 ++\n Documentation/git-fetch.txt     |   9 ++\n builtin/fetch.c                 |  88 ++++++++++++++----\n t/t5574-fetch-output.sh         | 159 ++++++++++++++++++++++++++++++++\n 4 files changed, 244 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt\nindex 622bd84768..41fc7ca3c6 100644\n--- a/Documentation/fetch-options.txt\n+++ b/Documentation/fetch-options.txt\n@@ -78,6 +78,13 @@ linkgit:git-config[1].\n --dry-run::\n \tShow what would be done, without making any changes.\n \n+--porcelain::\n+\tPrint the output to standard output in an easy-to-parse format for\n+\tscripts. See section OUTPUT in linkgit:git-fetch[1] for details.\n++\n+This is incompatible with `--recurse-submodules=[yes|on-demand]` and takes\n+precedence over the `fetch.output` config option.\n+\n ifndef::git-pull[]\n --[no-]write-fetch-head::\n \tWrite the list of remote refs fetched in the `FETCH_HEAD`\ndiff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt\nindex fba66f1460..f123139c58 100644\n--- a/Documentation/git-fetch.txt\n+++ b/Documentation/git-fetch.txt\n@@ -204,6 +204,15 @@ representing the status of a single ref. Each line is of the form:\n  <flag> <summary> <from> -> <to> [<reason>]\n -------------------------------\n \n+When using `--porcelain`, the output format is intended to be\n+machine-parseable. In contrast to the human-readable output formats it\n+thus prints to standard output instead of standard error. Each line is\n+of the form:\n+\n+-------------------------------\n+<flag> <old-object-id> <new-object-id> <local-reference>\n+-------------------------------\n+\n The status of up-to-date refs is shown only if the --verbose option is\n used.\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7a3d620c4c..462fc86b99 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -52,6 +52,7 @@ enum display_format {\n \tDISPLAY_FORMAT_UNKNOWN = 0,\n \tDISPLAY_FORMAT_FULL,\n \tDISPLAY_FORMAT_COMPACT,\n+\tDISPLAY_FORMAT_PORCELAIN,\n };\n \n struct display_state {\n@@ -754,6 +755,9 @@ static void display_state_init(struct display_state *display_state, struct ref *\n \t\tdisplay_state->refcol_width = refcol_width(ref_map,\n \t\t\t\t\t\t\t   display_state->format == DISPLAY_FORMAT_COMPACT);\n \t\tbreak;\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\t/* We don't need to precompute anything here. */\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t}\n@@ -824,8 +828,12 @@ static void print_compact(struct display_state *display_state,\n static void display_ref_update(struct display_state *display_state, char code,\n \t\t\t       const char *summary, const char *error,\n \t\t\t       const char *remote, const char *local,\n+\t\t\t       const struct object_id *old_oid,\n+\t\t\t       const struct object_id *new_oid,\n \t\t\t       int summary_width)\n {\n+\tFILE *f = stderr;\n+\n \tif (verbosity < 0)\n \t\treturn;\n \n@@ -858,12 +866,17 @@ static void display_ref_update(struct display_state *display_state, char code,\n \n \t\tbreak;\n \t}\n+\tcase DISPLAY_FORMAT_PORCELAIN:\n+\t\tstrbuf_addf(&display_state->buf, \"%c %s %s %s\", code,\n+\t\t\t    oid_to_hex(old_oid), oid_to_hex(new_oid), local);\n+\t\tf = stdout;\n+\t\tbreak;\n \tdefault:\n \t\tBUG(\"unexpected display format %d\", display_state->format);\n \t};\n \tstrbuf_addch(&display_state->buf, '\\n');\n \n-\tfputs(display_state->buf.buf, stderr);\n+\tfputs(display_state->buf.buf, f);\n }\n \n static int update_local_ref(struct ref *ref,\n@@ -881,7 +894,8 @@ static int update_local_ref(struct ref *ref,\n \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n \t\tif (verbosity > 0)\n \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 0;\n \t}\n \n@@ -894,7 +908,8 @@ static int update_local_ref(struct ref *ref,\n \t\t */\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t   _(\"can't fetch into checked-out branch\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n \n@@ -905,12 +920,14 @@ static int update_local_ref(struct ref *ref,\n \t\t\tr = s_update_ref(\"updating tag\", ref, transaction, 0);\n \t\t\tdisplay_ref_update(display_state, r ? '!' : 't', _(\"[tag update]\"),\n \t\t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn r;\n \t\t} else {\n \t\t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"),\n \t\t\t\t\t   _(\"would clobber existing tag\"),\n-\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\t\treturn 1;\n \t\t}\n \t}\n@@ -943,7 +960,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(msg, ref, transaction, 0);\n \t\tdisplay_ref_update(display_state, r ? '!' : '*', what,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn r;\n \t}\n \n@@ -965,7 +983,8 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"fast-forward\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : ' ', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : NULL,\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else if (force || ref->force) {\n@@ -977,12 +996,14 @@ static int update_local_ref(struct ref *ref,\n \t\tr = s_update_ref(\"forced-update\", ref, transaction, 1);\n \t\tdisplay_ref_update(display_state, r ? '!' : '+', quickref.buf,\n \t\t\t\t   r ? _(\"unable to update local ref\") : _(\"forced update\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\tstrbuf_release(&quickref);\n \t\treturn r;\n \t} else {\n \t\tdisplay_ref_update(display_state, '!', _(\"[rejected]\"), _(\"non-fast-forward\"),\n-\t\t\t\t   remote_ref->name, ref->name, summary_width);\n+\t\t\t\t   remote_ref->name, ref->name,\n+\t\t\t\t   &ref->old_oid, &ref->new_oid, summary_width);\n \t\treturn 1;\n \t}\n }\n@@ -1223,7 +1244,9 @@ static int store_updated_refs(struct display_state *display_state,\n \t\t\t\tdisplay_ref_update(display_state, '*',\n \t\t\t\t\t\t   *kind ? kind : \"branch\", NULL,\n \t\t\t\t\t\t   rm->name,\n-\t\t\t\t\t\t   \"FETCH_HEAD\", summary_width);\n+\t\t\t\t\t\t   \"FETCH_HEAD\",\n+\t\t\t\t\t\t   &rm->new_oid, &rm->old_oid,\n+\t\t\t\t\t\t   summary_width);\n \t\t\t}\n \t\t}\n \t}\n@@ -1363,6 +1386,7 @@ static int prune_refs(struct display_state *display_state,\n \t\tfor (ref = stale_refs; ref; ref = ref->next) {\n \t\t\tdisplay_ref_update(display_state, '-', _(\"[deleted]\"), NULL,\n \t\t\t\t\t   _(\"(none)\"), ref->name,\n+\t\t\t\t\t   &ref->new_oid, &ref->old_oid,\n \t\t\t\t\t   summary_width);\n \t\t\twarn_dangling_symref(stderr, dangling_msg, ref->name);\n \t\t}\n@@ -1795,7 +1819,8 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n \treturn 1;\n }\n \n-static void add_options_to_argv(struct strvec *argv)\n+static void add_options_to_argv(struct strvec *argv,\n+\t\t\t\tenum display_format format)\n {\n \tif (dry_run)\n \t\tstrvec_push(argv, \"--dry-run\");\n@@ -1831,6 +1856,8 @@ static void add_options_to_argv(struct strvec *argv)\n \t\tstrvec_push(argv, \"--ipv6\");\n \tif (!write_fetch_head)\n \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n+\tif (format == DISPLAY_FORMAT_PORCELAIN)\n+\t\tstrvec_pushf(argv, \"--porcelain\");\n }\n \n /* Fetch multiple remotes in parallel */\n@@ -1839,6 +1866,7 @@ struct parallel_fetch_state {\n \tconst char **argv;\n \tstruct string_list *remotes;\n \tint next, result;\n+\tenum display_format format;\n };\n \n static int fetch_next_remote(struct child_process *cp,\n@@ -1858,7 +1886,7 @@ static int fetch_next_remote(struct child_process *cp,\n \tstrvec_push(&cp->args, remote);\n \tcp->git_cmd = 1;\n \n-\tif (verbosity >= 0)\n+\tif (verbosity >= 0 && state->format != DISPLAY_FORMAT_PORCELAIN)\n \t\tprintf(_(\"Fetching %s\\n\"), remote);\n \n \treturn 1;\n@@ -1890,7 +1918,8 @@ static int fetch_finished(int result, struct strbuf *out,\n \treturn 0;\n }\n \n-static int fetch_multiple(struct string_list *list, int max_children)\n+static int fetch_multiple(struct string_list *list, int max_children,\n+\t\t\t  enum display_format format)\n {\n \tint i, result = 0;\n \tstruct strvec argv = STRVEC_INIT;\n@@ -1903,10 +1932,10 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \tstrvec_pushl(&argv, \"fetch\", \"--append\", \"--no-auto-gc\",\n \t\t     \"--no-write-commit-graph\", NULL);\n-\tadd_options_to_argv(&argv);\n+\tadd_options_to_argv(&argv, format);\n \n \tif (max_children != 1 && list->nr != 1) {\n-\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0 };\n+\t\tstruct parallel_fetch_state state = { argv.v, list, 0, 0, format };\n \t\tconst struct run_process_parallel_opts opts = {\n \t\t\t.tr2_category = \"fetch\",\n \t\t\t.tr2_label = \"parallel/fetch\",\n@@ -1930,7 +1959,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \n \t\t\tstrvec_pushv(&cmd.args, argv.v);\n \t\t\tstrvec_push(&cmd.args, name);\n-\t\t\tif (verbosity >= 0)\n+\t\t\tif (verbosity >= 0 && format != DISPLAY_FORMAT_PORCELAIN)\n \t\t\t\tprintf(_(\"Fetching %s\\n\"), name);\n \t\t\tcmd.git_cmd = 1;\n \t\t\tif (run_command(&cmd)) {\n@@ -2081,6 +2110,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint fetch_write_commit_graph = -1;\n \tint stdin_refspecs = 0;\n \tint negotiate_only = 0;\n+\tint porcelain = 0;\n \tint i;\n \n \tstruct option builtin_fetch_options[] = {\n@@ -2115,6 +2145,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n \t\t\t N_(\"dry run\")),\n+\t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n \t\tOPT_BOOL(0, \"write-fetch-head\", &write_fetch_head,\n \t\t\t N_(\"write fetched references to the FETCH_HEAD file\")),\n \t\tOPT_BOOL('k', \"keep\", &keep, N_(\"keep downloaded pack\")),\n@@ -2220,6 +2251,26 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tfetch_config_from_gitmodules(sfjc, rs);\n \t}\n \n+\n+\tif (porcelain) {\n+\t\tswitch (recurse_submodules_cli) {\n+\t\tcase RECURSE_SUBMODULES_OFF:\n+\t\tcase RECURSE_SUBMODULES_DEFAULT:\n+\t\t\t/*\n+\t\t\t * Reference updates in submodules would be ambiguous\n+\t\t\t * in porcelain mode, so we reject this combination.\n+\t\t\t */\n+\t\t\trecurse_submodules = RECURSE_SUBMODULES_OFF;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--porcelain\", \"--recurse-submodules\");\n+\t\t}\n+\n+\t\tconfig.display_format = DISPLAY_FORMAT_PORCELAIN;\n+\t}\n+\n \tif (negotiate_only && !negotiation_tip.nr)\n \t\tdie(_(\"--negotiate-only needs one or more --negotiation-tip=*\"));\n \n@@ -2339,10 +2390,9 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\t\tmax_children = fetch_parallel_config;\n \n \t\t/* TODO should this also die if we have a previous partial-clone? */\n-\t\tresult = fetch_multiple(&list, max_children);\n+\t\tresult = fetch_multiple(&list, max_children, config.display_format);\n \t}\n \n-\n \t/*\n \t * This is only needed after fetch_one(), which does not fetch\n \t * submodules by itself.\n@@ -2361,7 +2411,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t\tif (max_children < 0)\n \t\t\tmax_children = fetch_parallel_config;\n \n-\t\tadd_options_to_argv(&options);\n+\t\tadd_options_to_argv(&options, config.display_format);\n \t\tresult = fetch_submodules(the_repository,\n \t\t\t\t\t  &options,\n \t\t\t\t\t  submodule_prefix,\ndiff --git a/t/t5574-fetch-output.sh b/t/t5574-fetch-output.sh\nindex 9890f6f381..90e6dcb9a7 100755\n--- a/t/t5574-fetch-output.sh\n+++ b/t/t5574-fetch-output.sh\n@@ -61,6 +61,141 @@ test_expect_success 'fetch compact output' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch porcelain output' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\t# Set up a bunch of references that we can use to demonstrate different\n+\t# kinds of flag symbols in the output format.\n+\tMAIN_OLD=$(git rev-parse HEAD) &&\n+\tgit branch \"fast-forward\" &&\n+\tgit branch \"deleted-branch\" &&\n+\tgit checkout -b force-updated &&\n+\ttest_commit --no-tag force-update-old &&\n+\tFORCE_UPDATED_OLD=$(git rev-parse HEAD) &&\n+\tgit checkout main &&\n+\n+\t# Clone and pre-seed the repositories. We fetch references into two\n+\t# namespaces so that we can test that rejected and force-updated\n+\t# references are reported properly.\n+\trefspecs=\"refs/heads/*:refs/unforced/* +refs/heads/*:refs/forced/*\" &&\n+\tgit clone . porcelain &&\n+\tgit -C porcelain fetch origin $refspecs &&\n+\n+\t# Now that we have set up the client repositories we can change our\n+\t# local references.\n+\tgit branch new-branch &&\n+\tgit branch -d deleted-branch &&\n+\tgit checkout fast-forward &&\n+\ttest_commit --no-tag fast-forward-new &&\n+\tFAST_FORWARD_NEW=$(git rev-parse HEAD) &&\n+\tgit checkout force-updated &&\n+\tgit reset --hard HEAD~ &&\n+\ttest_commit --no-tag force-update-new &&\n+\tFORCE_UPDATED_NEW=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t- $MAIN_OLD $ZERO_OID refs/forced/deleted-branch\n+\t- $MAIN_OLD $ZERO_OID refs/unforced/deleted-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/unforced/fast-forward\n+\t! $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/unforced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/unforced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/forced/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/forced/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/forced/new-branch\n+\t  $MAIN_OLD $FAST_FORWARD_NEW refs/remotes/origin/fast-forward\n+\t+ $FORCE_UPDATED_OLD $FORCE_UPDATED_NEW refs/remotes/origin/force-updated\n+\t* $ZERO_OID $MAIN_OLD refs/remotes/origin/new-branch\n+\tEOF\n+\n+\t# Execute a dry-run fetch first. We do this to assert that the dry-run\n+\t# and non-dry-run fetches produces the same output. Execution of the\n+\t# fetch is expected to fail as we have a rejected reference update.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --dry-run --prune origin $refspecs >actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# And now we perform a non-dry-run fetch.\n+\ttest_must_fail git -C porcelain fetch \\\n+\t\t--porcelain --prune origin $refspecs >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain with multiple remotes' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tgit switch --create multiple-remotes &&\n+\tgit clone . porcelain &&\n+\tgit -C porcelain remote add second-remote \"$PWD\" &&\n+\tgit -C porcelain fetch second-remote &&\n+\n+\ttest_commit --no-tag multi-commit &&\n+\told_commit=$(git rev-parse HEAD~) &&\n+\tnew_commit=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t  $old_commit $new_commit refs/remotes/origin/multiple-remotes\n+\t  $old_commit $new_commit refs/remotes/second-remote/multiple-remotes\n+\tEOF\n+\n+\tgit -C porcelain fetch --porcelain --all >actual 2>stderr &&\n+\ttest_cmp expect actual &&\n+\ttest_must_be_empty stderr\n+'\n+\n+test_expect_success 'fetch porcelain refuses to work with submodules' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tcat >expect <<-EOF &&\n+\tfatal: options ${SQ}--porcelain${SQ} and ${SQ}--recurse-submodules${SQ} cannot be used together\n+\tEOF\n+\n+\tgit init porcelain &&\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=yes 2>stderr &&\n+\ttest_cmp expect stderr &&\n+\n+\ttest_must_fail git -C porcelain fetch --porcelain --recurse-submodules=on-demand 2>stderr &&\n+\ttest_cmp expect stderr\n+'\n+\n+test_expect_success 'fetch porcelain overrides fetch.output config' '\n+\ttest_when_finished \"rm -rf porcelain\" &&\n+\n+\tgit switch --create config-override &&\n+\tgit clone . porcelain &&\n+\ttest_commit new-commit &&\n+\told_commit=$(git rev-parse HEAD~) &&\n+\tnew_commit=$(git rev-parse HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\t  $old_commit $new_commit refs/remotes/origin/config-override\n+\t* $ZERO_OID $new_commit refs/tags/new-commit\n+\tEOF\n+\n+\tgit -C porcelain -c fetch.output=compact fetch --porcelain >stdout 2>stderr &&\n+\ttest_must_be_empty stderr &&\n+\ttest_cmp expect stdout\n+'\n+\n+test_expect_success 'fetch --no-porcelain overrides previous --porcelain' '\n+\ttest_when_finished \"rm -rf no-porcelain\" &&\n+\n+\tgit switch --create no-porcelain &&\n+\tgit clone . no-porcelain &&\n+\ttest_commit --no-tag no-porcelain &&\n+\told_commit=$(git rev-parse --short HEAD~) &&\n+\tnew_commit=$(git rev-parse --short HEAD) &&\n+\n+\tcat >expect <<-EOF &&\n+\tFrom $(test-tool path-utils real_path .)/.\n+\t   $old_commit..$new_commit  no-porcelain -> origin/no-porcelain\n+\tEOF\n+\n+\tgit -C no-porcelain fetch --porcelain --no-porcelain >stdout 2>stderr &&\n+\ttest_cmp expect stderr &&\n+\ttest_must_be_empty stdout\n+'\n+\n test_expect_success 'fetch output with HEAD' '\n \ttest_when_finished \"rm -rf head\" &&\n \tgit clone . head &&\n@@ -90,6 +225,30 @@ test_expect_success 'fetch output with HEAD' '\n \ttest_cmp expect actual.err\n '\n \n+test_expect_success 'fetch porcelain output with HEAD' '\n+\ttest_when_finished \"rm -rf head\" &&\n+\tgit clone . head &&\n+\tCOMMIT_ID=$(git rev-parse HEAD) &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID FETCH_HEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain --dry-run origin HEAD:foo >actual &&\n+\tcat >expect <<-EOF &&\n+\t* $ZERO_OID $COMMIT_ID refs/heads/foo\n+\tEOF\n+\ttest_cmp expect actual &&\n+\n+\tgit -C head fetch --porcelain origin HEAD:foo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'fetch output with object ID' '\n \ttest_when_finished \"rm -rf object-id\" &&\n \tgit clone . object-id &&\n-- \n2.40.1\n\n"},{"id":"476967","messageId":"ZFuPZMK-xtygmZ7J@ncase","threadId":"59619","inReplyTo":"kl6l5y911ht4.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v4 1/8] fetch: fix `--no-recurse-submodules` with multi-remote fetches","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:44Z","receivedAt":"2023-05-10T12:35:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 11:27:35AM -0700, Glen Choo wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> >> +\t\tgit config fetch.parallel 0 &&\n> >\n> > Is this necessary for the purpose of the test, though?  It should\n> > not hurt, but we do not require the end-users to set it in real life\n> > for the parallel fetching to work, either, right?\n> \n> IIUC it would make the test output deterministic if we fetched from both\n> remotes. That doesn't happen here though, so I guess it's not doing\n> anything right now.\n\nRight, will drop.\n\n> >> +\t\tgit fetch --all --no-recurse-submodules 2>../actual\n> >> +\t) &&\n> >> +\n> >> +\tcat >expect <<-EOF &&\n> >> +\tFrom ../src\n> >> +\t * [new branch]      master     -> secondary/master\n> >> +\tEOF\n> >> +\ttest_cmp expect actual\n> >> +'\n> >\n> > In the context of a series that attempts to introduce a new stable\n> > output format for machine consumption, which implies the traditional\n> > output can change to match human users' preference, this test feels\n> > a bit brittle, but let's wait until the end of the series to judge\n> > that.\n> \n> I also find it a bit brittle to assert on the whole output when this\n> test is about checking that we do not fetch the superproject.\n> \n> Is there a reason you didn't go with the \"grep for submodule lines\"\n> approach in the previous tests? If it's about catching regressions, IMO\n> your PATCH 2/8 does a good enough job of doing that.\n> \n> Wondering out loud, I wonder if it makes sense for us to make a bigger\n> distinction between \"tests whose purpose is to guard against unexpected\n> changes in output\" (i.e. snapshot tests) vs \"tests that happen to use\n> output as a way to assert behavior\" (i.e. 'regular' behavioral tests).\n> Many GUI app codebases have such a distinction and have different best\n> practices around them.\n\nFair enough, will switch to `! grep \"Fetching submodule\"`.\n\nPatrick\n"},{"id":"476968","messageId":"ZFuPbJZ4jtFfX7iY@ncase","threadId":"59619","inReplyTo":"xmqqv8h1xu7q.fsf@gitster.g","subject":"Re: [PATCH v4 3/8] fetch: add a test to exercise invalid output formats","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:52Z","receivedAt":"2023-05-10T12:35:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 10:58:33AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Add a testcase that exercises the logic when an invalid output format is\n> > passed via the `fetch.output` configuration.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  t/t5574-fetch-output.sh | 19 +++++++++++++++++++\n> >  1 file changed, 19 insertions(+)\n> \n> It makes perfect sense to make sure that invalid input gets rejected\n> and the command exits with non-zero status, and it is probably a\n> good thing that the end-user sees a message that explains why the\n> particular input is rejected (even though it adds one more thing\n> that needs to be updated when the message gets reworded).\n> \n> But do we need to insist on no output to the standard output stream\n> when the command errors out?\n> \n> Other than that, looking good.\n\nNot really, no. Will change.\n\nPatrick\n"},{"id":"476969","messageId":"ZFuPcfeqCbJo4XcS@ncase","threadId":"59619","inReplyTo":"xmqqednpxq27.fsf@gitster.g","subject":"Re: [PATCH v4 4/8] fetch: fix missing from-reference when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:34:57Z","receivedAt":"2023-05-10T12:35:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 12:28:16PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > But it is not intended when displaying the updated references and would\n> > cause us to miss the left-hand side of the displayed reference update:\n> >\n> > ```\n> > $ git fetch origin HEAD:foo\n> > From https://github.com/git/git\n> >  * [new ref]                          -> foo\n> > ```\n> > The HEAD string is clearly missing from the left-hand side of the arrow,\n> > which is further stressed by the point that the following commands show\n> > the left-hand side as expected:\n> >\n> > ```\n> > $ git fetch origin HEAD\n> > From https://github.com/git/git\n> >  * branch                  HEAD       -> FETCH_HEAD\n> \n> I do not mind being explicit and showing HEAD in this case for the\n> sake of consistency.\n> \n> But speaking for the past developers, it was deliberate to omit what\n> is common from the output to make it more terse, IIRC, and I think\n> it is unfair to call it a \"BUG\".\n> \n> Back when we wrote git-fetch-script, the output was a lot more\n> verbose, and through efforts like 165f3902 (git-fetch: more terse\n> fetch output, 2007-11-03) and numerous others over time, we got to\n> the current output.\n\nThat's fair. It's still not quite clear whether this behaviour is in\nfact intentional though. Quoting 165f3902 (git-fetch: more terse\nfetch output, 2007-11-03), the weird corner case is not documented:\n\n    This makes the fetch output much more terse and prettier on a 80 column\n    display, based on a consensus reached on the mailing list.  Here's an\n    example output:\n\n    Receiving objects: 100% (5439/5439), 1.60 MiB | 636 KiB/s, done.\n    Resolving deltas: 100% (4604/4604), done.\n    From git://git.kernel.org/pub/scm/git/git\n     ! [rejected]        html -> origin/html  (non fast forward)\n       136e631..f45e867  maint -> origin/maint  (fast forward)\n       9850e2e..44dd7e0  man -> origin/man  (fast forward)\n       3e4bb08..e3d6d56  master -> origin/master  (fast forward)\n       fa3665c..536f64a  next -> origin/next  (fast forward)\n     + 4f6d9d6...768326f pu -> origin/pu  (forced update)\n     * [new branch]      todo -> origin/todo\n\nI've reformulated the commit message to talk about an inconsistency\ninstead of a bug.\n\nPatrick\n"},{"id":"476970","messageId":"ZFuPd8sPOFcbFohW@ncase","threadId":"59619","inReplyTo":"xmqq5y91xnou.fsf@gitster.g","subject":"Re: [PATCH v4 5/8] fetch: introduce `display_format` enum","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:35:03Z","receivedAt":"2023-05-10T12:35:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 01:19:29PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> > -\tdisplay_state->refcol_width = 10;\n> > -\tfor (rm = ref_map; rm; rm = rm->next) {\n> > -\t\tint width;\n> > +\tswitch (display_state->format) {\n> > +\tcase DISPLAY_FORMAT_FULL:\n> > +\tcase DISPLAY_FORMAT_COMPACT: {\n> > +\t\tstruct ref *rm;\n> >  \n> > -\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n> > -\t\t    !rm->peer_ref ||\n> > -\t\t    !strcmp(rm->name, \"HEAD\"))\n> > -\t\t\tcontinue;\n> > +\t\tdisplay_state->refcol_width = 10;\n> > +\t\tfor (rm = ref_map; rm; rm = rm->next) {\n> > +\t\t\tint width;\n> >  \n> > -\t\twidth = refcol_width(rm, display_state->compact_format);\n> > +\t\t\tif (rm->status == REF_STATUS_REJECT_SHALLOW ||\n> > +\t\t\t    !rm->peer_ref ||\n> > +\t\t\t    !strcmp(rm->name, \"HEAD\"))\n> > +\t\t\t\tcontinue;\n> >  \n> > -\t\t/*\n> > -\t\t * Not precise calculation for compact mode because '*' can\n> > -\t\t * appear on the left hand side of '->' and shrink the column\n> > -\t\t * back.\n> > -\t\t */\n> > -\t\tif (display_state->refcol_width < width)\n> > -\t\t\tdisplay_state->refcol_width = width;\n> > +\t\t\twidth = refcol_width(rm, display_state->format == DISPLAY_FORMAT_COMPACT);\n> > +\n> > +\t\t\t/*\n> > +\t\t\t * Not precise calculation for compact mode because '*' can\n> > +\t\t\t * appear on the left hand side of '->' and shrink the column\n> > +\t\t\t * back.\n> > +\t\t\t */\n> > +\t\t\tif (display_state->refcol_width < width)\n> > +\t\t\t\tdisplay_state->refcol_width = width;\n> > +\t\t}\n> > +\n> > +\t\tbreak;\n> > +\t}\n> > +\tdefault:\n> > +\t\tBUG(\"unexpected display format %d\", display_state->format);\n> >  \t}\n> \n> Due to reindentation, the patch is noisier than what it does (which\n> should be \"nothing, other than allowing another value in the .format\n> member\").\n> \n> It makes me wonder if it would make it easier to read to move the\n> bulk of this code to a helper function.  If we are to give a name to\n> what is being done in the above hunk, what would it be?  It computes\n> display->refcol_width in which all records would fit, but presumably\n> if we are to add more things to be shown per ref and align them in a\n> simlar way, we would compute widths for these other things there as\n> well.  Perhaps compute_display_alignment() or somesuch?\n\nThe code already has such a function and calls it `refcol_width()`, but\nit only computes the width for a single reference. The most natural\nthing to do here would thus be to merge the loop over the references\ninto that function. This would also allow us to skip some weirdness,\nlike the fact that we skip some references inside `refcol_width()` while\nwe skip other references in the `ref_map` loop.\n\nThis refactoring is also quite noisy, but it makes the code simpler\noverall and will make the patch introducing the enum less so.\n\nPatrick\n"},{"id":"476971","messageId":"ZFuPfYBqFcCza21J@ncase","threadId":"59619","inReplyTo":"kl6l1qjp16k6.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v4 6/8] fetch: move display format parsing into main function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:35:09Z","receivedAt":"2023-05-10T12:36:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 03:30:33PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > with the `fetch.output` config key set. We're thus going to introduce a\n> > new `--output-format` switch for git-fetch(1) so that the output format\n> > can be configured more directly.\n> \n> This is stale as of v3, since isn't named --output-format any more. Let\n> me see if there are other instances of this.\n> \n> (I should have caught this earlier; I only saw it because Junio quoted\n> it in a reply of his.)\n\nGood catch, fixed now.\n\nPatrick\n"},{"id":"476972","messageId":"ZFuPgh2ad-Cw1r3P@ncase","threadId":"59619","inReplyTo":"xmqqjzxhw802.fsf@gitster.g","subject":"Re: [PATCH v4 8/8] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-10T12:35:14Z","receivedAt":"2023-05-10T12:36:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, May 09, 2023 at 01:43:41PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Considering that both multi-remote and submodule fetches are user-facing\n> > features, using them in conjunction with `--porcelain` that is intended\n> > for scripting purposes is likely not going to be useful in the majority\n> > of cases. With that in mind these restrictions feel acceptable. If\n> > usecases for either of these come up in the future though it is easy\n> > enough to add a new \"porcelain-v2\" format that adds this information.\n> \n> Two steps ago, the proposed log message still mentioned \"--output-format\",\n> which may want to be proofread again and revised.\n\nIt's a relict from previous iterations. I've fixed the preceding patch\nto talk about `--porcelain` instead.\n\n> > @@ -1786,7 +1810,8 @@ static int add_remote_or_group(const char *name, struct string_list *list)\n> >  \treturn 1;\n> >  }\n> >  \n> > -static void add_options_to_argv(struct strvec *argv)\n> > +static void add_options_to_argv(struct strvec *argv,\n> > +\t\t\t\tenum display_format format)\n> >  {\n> >  \tif (dry_run)\n> >  \t\tstrvec_push(argv, \"--dry-run\");\n> > @@ -1822,6 +1847,8 @@ static void add_options_to_argv(struct strvec *argv)\n> >  \t\tstrvec_push(argv, \"--ipv6\");\n> >  \tif (!write_fetch_head)\n> >  \t\tstrvec_push(argv, \"--no-write-fetch-head\");\n> > +\tif (format == DISPLAY_FORMAT_PORCELAIN)\n> > +\t\tstrvec_pushf(argv, \"--porcelain\");\n> >  }\n> \n> Hmph.  \n> \n> [PATCH 9/8] may want to also introduce and pass down the\n> \"--output-format=full/compact\" option, but that is clearly outside\n> of the scope of this step.\n\nWe don't have `--output-format` now though, but only the `--porcelain`\nswitch. The only way to configure \"full\"/\"compact\" output formats is via\nthe \"fetch.output\" config variable right now.\n\n> > +static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)\n> > +{\n> > +\tenum display_format *format = opt->value;\n> > +\t*format = DISPLAY_FORMAT_PORCELAIN;\n> > +\treturn 0;\n> > +}\n> \n> Lack of \"if (unset) ...\" worries me.  Shouldn't the code allow\n> \n> \tgit fetch --porcelain --no-porcelain\n> \n> to defeat an earlier one and revert back to the default?\n\nWe pass `PARSE_OPT_NOARG|PARSE_OPT_NONEG`, so this isn't really much of\na concern. But yeah, it wouldn't allow `--no-porcelain`, and supporting\nit is trivial enough to do. I'll change this to a `OPT_BOOL()`.\n\nPatrick\n"},{"id":"476980","messageId":"xmqqwn1g3vvk.fsf@gitster.g","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-10T18:05:19Z","receivedAt":"2023-05-10T18:05:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>     - Patch 3/9: Added a test to verify that `git fetch -c fetch.output`\n>       without a value set fails as expected. Also dropped the tests that\n>       checked whether stdout was empty.\n\nThank you for the attention to this small detail of valueless\nconfiguration variable definition.\n\n>     - Patch 4/9: Reformulated the commit message to treat the missing\n>       left-hand side of displayed references as an inconsistency instead\n>       of a bug. I've also added a testcase to verify that direct OID\n>       fetches continue to work as expected.\n\nAgain, the direct OID fetch is a good thing to test here.  I noticed\nthat the test added here insists that the standard output stream is\nempty when the command errors out, which is not consistent with [3/9]\nabove.\n\n>     - Patch 5/9: New patch that makes calculation of the table width for\n>       displayed reference updates self-contained in `refcol_width()`.\n>       This is a preparatory refactoring that should make patch 6/9\n>       easier to review.\n\nIt is an excellent idea to avoid calls to refcol_width() for each\nref that gets shown and make the helper responsible for computing\nthe required maxwidth.  The result indeed has become easier to\nfollow as you mentioned in your response to my review on the\nprevious round.\n\n>     - Patch 7/9: Refactored the code to parse the \"fetch.output\" config\n>       variable inside of `git_fetch_config()` before we parse command\n>       line options. Also fixed that the commit message was still\n>       referring to `--output-format=porcelain` instead of the new\n>       `--porcelain` switch.\n\nAs a standalone step, it leaves an impression that the step makes\nthe way we handle the output-format configuration variable\ninconsistent with the way we handle the other configuration\nvariables, but I think it is a good place to stop for the purpose of\nthis topic.  It lays a good foundation for future clean-up after the\ndust settles from this topic---we may want to move global variables\nassigned in the git_fetch_config() into the fetch_config structure.\n\n>     - Patch 9/9: The `--porcelain` option is now a simple `OPT_BOOL()`\n>       that can be negated. Added a test that `--no-porcelain` works as\n>       expected.\n\nOK, this time the familiar \"prepare a variable to its default, let\nconfig callback to overwrite it by reading configuration variables,\nand then let the command line option override it\" is used and the\nresult easy to follow.  I do not think .display_format is never\nassigned DISPLAY_FORMAT_UNKNOWN with this change, so [6/9] could\nlose the value from the enum, I think.  The defensive switch\nstatement that has BUG() to notice an erroneous caller that pass\nvalues other than DISPLAY_FORMAT_{FULL,COMPACT} is still a good\nidea.\n\nLooking good.\n\nThanks.\n"},{"id":"477030","messageId":"ZFzMDCK8rprq6qUs@ncase","threadId":"59619","inReplyTo":"xmqqwn1g3vvk.fsf@gitster.g","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-11T11:05:48Z","receivedAt":"2023-05-11T11:06:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, May 10, 2023 at 11:05:19AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> >     - Patch 4/9: Reformulated the commit message to treat the missing\n> >       left-hand side of displayed references as an inconsistency instead\n> >       of a bug. I've also added a testcase to verify that direct OID\n> >       fetches continue to work as expected.\n> \n> Again, the direct OID fetch is a good thing to test here.  I noticed\n> that the test added here insists that the standard output stream is\n> empty when the command errors out, which is not consistent with [3/9]\n> above.\n\nI think you misread the test: yes, we do test stdout and stderr\nseparately and in many cases assert that stdout is in fact empty. But\nnone of the added tests are about failing commands. So given that:\n\n    - The added tests explicitly are about verifying the output format.\n\n    - The distinction between stdout and stderr matters.\n\n    - The distinction matters even more with the addition of\n      `--porcelain`.\n\nI think that explicitly verifing both output streams is the correct\nthing to do.\n\n> >     - Patch 9/9: The `--porcelain` option is now a simple `OPT_BOOL()`\n> >       that can be negated. Added a test that `--no-porcelain` works as\n> >       expected.\n> \n> OK, this time the familiar \"prepare a variable to its default, let\n> config callback to overwrite it by reading configuration variables,\n> and then let the command line option override it\" is used and the\n> result easy to follow.  I do not think .display_format is never\n> assigned DISPLAY_FORMAT_UNKNOWN with this change, so [6/9] could\n> lose the value from the enum, I think.  The defensive switch\n> statement that has BUG() to notice an erroneous caller that pass\n> values other than DISPLAY_FORMAT_{FULL,COMPACT} is still a good\n> idea.\n\nAh, right, `DISPLAY_FORMAT_UNKNOWN` isn't really needed anymore. I think\nit's still good to have valid values of the enum start with `1` so that\nit becomes easier to detect cases where we accidentally use a default\ninitialized variable. But that can be achieved without giving the\ndefault value an explicit name.\n\nI'll refrain from sending a new version just to remove this constant as\nit doesn't really feel worth it, though. But please, let me know in case\nyou disagree and I'll send an updated version.\n\nThanks\nPatrick\n"},{"id":"477044","messageId":"xmqqilcyyfly.fsf@gitster.g","threadId":"59619","inReplyTo":"ZFzMDCK8rprq6qUs@ncase","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-11T16:53:13Z","receivedAt":"2023-05-11T16:53:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, May 10, 2023 at 11:05:19AM -0700, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n> [snip]\n>> >     - Patch 4/9: Reformulated the commit message to treat the missing\n>> >       left-hand side of displayed references as an inconsistency instead\n>> >       of a bug. I've also added a testcase to verify that direct OID\n>> >       fetches continue to work as expected.\n>> \n>> Again, the direct OID fetch is a good thing to test here.  I noticed\n>> that the test added here insists that the standard output stream is\n>> empty when the command errors out, which is not consistent with [3/9]\n>> above.\n>\n> I think you misread the test: yes, we do test stdout and stderr\n> separately and in many cases assert that stdout is in fact empty. But\n> none of the added tests are about failing commands.\n\nAh, you are absolutely right. Sorry for the noise---I do not have an\nobjection to ensure that program output in the successful case is\npredictable.  My main concern is to avoid giving unneeded assurance\nin the failing case.\n\n>> assigned DISPLAY_FORMAT_UNKNOWN with this change, so [6/9] could\n>> lose the value from the enum, I think.  The defensive switch\n>> statement that has BUG() to notice an erroneous caller that pass\n>> values other than DISPLAY_FORMAT_{FULL,COMPACT} is still a good\n>> idea.\n>\n> Ah, right, `DISPLAY_FORMAT_UNKNOWN` isn't really needed anymore. I think\n> it's still good to have valid values of the enum start with `1` so that\n> it becomes easier to detect cases where we accidentally use a default\n> initialized variable. But that can be achieved without giving the\n> default value an explicit name.\n\nUnless we explicitly take advantage of UNKNOWN being 0 and perform\nclever defaulting, the result is far safer if you get rid of it, and\ninstead give 0 to a real choice that is used as the default, for two\nreasons: (1) naturally 0 initialization work fine, (2) we have one\nless enum constant people (or editor's auto-completer) assign to a\nvariable by mistake instead of a real one they intended to.  I agree\nwith the last sentence of your paragraph above.\n"},{"id":"477050","messageId":"645d24e37eadb_26011a29488@chronos.notmuch","threadId":"59619","inReplyTo":"ZFzMDCK8rprq6qUs@ncase","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2023-05-11T17:24:51Z","receivedAt":"2023-05-11T17:24:58Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Patrick Steinhardt wrote:\n> On Wed, May 10, 2023 at 11:05:19AM -0700, Junio C Hamano wrote:\n\n> > OK, this time the familiar \"prepare a variable to its default, let\n> > config callback to overwrite it by reading configuration variables,\n> > and then let the command line option override it\" is used and the\n> > result easy to follow.  I do not think .display_format is never\n> > assigned DISPLAY_FORMAT_UNKNOWN with this change, so [6/9] could\n> > lose the value from the enum, I think.  The defensive switch\n> > statement that has BUG() to notice an erroneous caller that pass\n> > values other than DISPLAY_FORMAT_{FULL,COMPACT} is still a good\n> > idea.\n> \n> Ah, right, `DISPLAY_FORMAT_UNKNOWN` isn't really needed anymore. I think\n> it's still good to have valid values of the enum start with `1` so that\n> it becomes easier to detect cases where we accidentally use a default\n> initialized variable. But that can be achieved without giving the\n> default value an explicit name.\n\nI think it's standard to have an element like that. It could be UNKNOWN, NULL,\nINVALID, or even DEFAULT.\n\nFor example, you could have code like this:\n\n if (format == DISPLAY_FORMAT_DEFAULT)\n        format = DISPLAY_FORMAT_FULL;\n\nThis would distinguish cases in which the user did not specify a display format\nand we choose a default, from those where the user explicitely chose the format\nthat happens to be the default.\n\n-- \nFelipe Contreras\n"},{"id":"477116","messageId":"kl6lmt2az9ms.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"2cc7318697ef6062f6deadf7a22feb26c2c829e8.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 4/9] fetch: print left-hand side when fetching HEAD:foo","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-12T00:16:59Z","receivedAt":"2023-05-12T00:17:04Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> This behaviour has existed ever since the table-based output has been\n> introduced for git-fetch(1) via 165f390250 (git-fetch: more terse fetch\n> output, 2007-11-03) and was never explicitly documented either in the\n> commit message or in any of our tests. So while it may not be a bug per\n> se, it feels like a weird inconsistency and not like it was a concious\n> design decision.\n\nThe change seems well-justified. I agree that it does feel strange and\nunintended. I was really surprised to learn that it was not a bug.\n"},{"id":"477118","messageId":"kl6lilcyz855.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"bb1a591c2f46b78eb26eb72ec677c65d10cb714c.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 5/9] fetch: refactor calculation of the display table width","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-12T00:49:10Z","receivedAt":"2023-05-12T00:49:15Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When displaying reference updates, we try to print the references in a\n> neat table. As the table's width is determined its contents we thus need\n> to precalculate the overall width before we can start printing updated\n> references.\n>\n> The calculation is driven by `display_state_init()`, which invokes\n> `refcol_width()` for every reference that is to be printed. This split\n> is somewhat confusing. For one, we filter references that shall be\n> attributed to the overall width in both places. And second, we\n> needlessly recalculate the maximum line length based on the terminal\n> columns and display format for every reference.\n>\n> Refactor the code so that the complete width calculations are neatly\n> contained in `refcol_width()`.\n\nThrough no fault of yours, I have to admit that I found this refactor\nquite hard to read. I ended up redoing the refactor and ended up with a\nresult very similar to yours. That was probably overkill since we have\npretty extensive tests in this area, but I'm quite happy with the change\nsince it's far more readable.\n\n> -\t/*\n> -\t * rough estimation to see if the output line is too long and\n> -\t * should not be counted (we can't do precise calculation\n> -\t * anyway because we don't know if the error explanation part\n> -\t * will be printed in update_local_ref)\n> -\t */\n> -\tif (compact_format) {\n> -\t\tllen = 0;\n> +\tmax = term_columns();\n> +\tif (compact_format)\n>  \t\tmax = max * 2 / 3;\n> -\t}\n> -\tlen = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen;\n> -\tif (len >= max)\n> -\t\treturn 0;\n\nThe one thing that changed for the better (vs keeping the lines in the\nsame order as before) is that this comment that used to be anchored on\n\"if (compact_format) {\"...\n\n> +\t\t/*\n> +\t\t * rough estimation to see if the output line is too long and\n> +\t\t * should not be counted (we can't do precise calculation\n> +\t\t * anyway because we don't know if the error explanation part\n> +\t\t * will be printed in update_local_ref)\n> +\t\t */\n> +\t\tlen = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen;\n> +\t\tif (len >= max)\n> +\t\t\tcontinue;\n\nis now more accurately anchored to the check that throws away the line.\n\nLooks good.\n"},{"id":"477119","messageId":"kl6lfs82z7jg.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"d49152c22068d18cf9f8a2f1e5fe92a3b1f13e37.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 9/9] fetch: introduce machine-parseable \"porcelain\" output format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-12T01:02:11Z","receivedAt":"2023-05-12T01:02:17Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +test_expect_success 'fetch --no-porcelain overrides previous --porcelain' '\n> +\ttest_when_finished \"rm -rf no-porcelain\" &&\n> +\n> +\tgit switch --create no-porcelain &&\n> +\tgit clone . no-porcelain &&\n> +\ttest_commit --no-tag no-porcelain &&\n> +\told_commit=$(git rev-parse --short HEAD~) &&\n> +\tnew_commit=$(git rev-parse --short HEAD) &&\n> +\n> +\tcat >expect <<-EOF &&\n> +\tFrom $(test-tool path-utils real_path .)/.\n> +\t   $old_commit..$new_commit  no-porcelain -> origin/no-porcelain\n> +\tEOF\n> +\n> +\tgit -C no-porcelain fetch --porcelain --no-porcelain >stdout 2>stderr &&\n> +\ttest_cmp expect stderr &&\n> +\ttest_must_be_empty stdout\n> +'\n\n(This shouldn't block the series) As a matter of taste, it feels like\noverkill to construct the whole output to check if --no-porcelain wins.\nMaybe in a future cleanup, we would go through the tests in this file\nand see where it is really important that we have the exact output and\nwhere it is not.\n"},{"id":"477120","messageId":"kl6lcz36z780.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59619","inReplyTo":"cover.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-05-12T01:09:03Z","receivedAt":"2023-05-12T01:09:09Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Changes compared to v4:\n>\n>     - Patch 1/9: Simplified the test as proposed by Junio and Glen.\n>\n>     - Patch 3/9: Added a test to verify that `git fetch -c fetch.output`\n>       without a value set fails as expected. Also dropped the tests that\n>       checked whether stdout was empty.\n>\n>     - Patch 4/9: Reformulated the commit message to treat the missing\n>       left-hand side of displayed references as an inconsistency instead\n>       of a bug. I've also added a testcase to verify that direct OID\n>       fetches continue to work as expected.\n>\n>     - Patch 5/9: New patch that makes calculation of the table width for\n>       displayed reference updates self-contained in `refcol_width()`.\n>       This is a preparatory refactoring that should make patch 6/9\n>       easier to review.\n>\n>     - Patch 7/9: Refactored the code to parse the \"fetch.output\" config\n>       variable inside of `git_fetch_config()` before we parse command\n>       line options. Also fixed that the commit message was still\n>       referring to `--output-format=porcelain` instead of the new\n>       `--porcelain` switch.\n>\n>     - Patch 9/9: The `--porcelain` option is now a simple `OPT_BOOL()`\n>       that can be negated. Added a test that `--no-porcelain` works as\n>       expected.\n\nI didn't spot any blocking issues in this version, and the various\nimprovements (especially 6-7/9) are really welcome. I also read through\nJunio's comments, but I didn't spot anything that I thought should block\nthe series, so I'm happy to leave\n\n  Reviewed-by: Glen Choo <chooglen@google.com>\n"},{"id":"477161","messageId":"ZF3n2yzfFKROBTza@ncase","threadId":"59619","inReplyTo":"kl6lcz36z780.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v5 0/9] fetch: introduce machine-parseable output","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-12T07:16:43Z","receivedAt":"2023-05-12T07:17:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, May 11, 2023 at 06:09:03PM -0700, Glen Choo wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Changes compared to v4:\n> >\n> >     - Patch 1/9: Simplified the test as proposed by Junio and Glen.\n> >\n> >     - Patch 3/9: Added a test to verify that `git fetch -c fetch.output`\n> >       without a value set fails as expected. Also dropped the tests that\n> >       checked whether stdout was empty.\n> >\n> >     - Patch 4/9: Reformulated the commit message to treat the missing\n> >       left-hand side of displayed references as an inconsistency instead\n> >       of a bug. I've also added a testcase to verify that direct OID\n> >       fetches continue to work as expected.\n> >\n> >     - Patch 5/9: New patch that makes calculation of the table width for\n> >       displayed reference updates self-contained in `refcol_width()`.\n> >       This is a preparatory refactoring that should make patch 6/9\n> >       easier to review.\n> >\n> >     - Patch 7/9: Refactored the code to parse the \"fetch.output\" config\n> >       variable inside of `git_fetch_config()` before we parse command\n> >       line options. Also fixed that the commit message was still\n> >       referring to `--output-format=porcelain` instead of the new\n> >       `--porcelain` switch.\n> >\n> >     - Patch 9/9: The `--porcelain` option is now a simple `OPT_BOOL()`\n> >       that can be negated. Added a test that `--no-porcelain` works as\n> >       expected.\n> \n> I didn't spot any blocking issues in this version, and the various\n> improvements (especially 6-7/9) are really welcome. I also read through\n> Junio's comments, but I didn't spot anything that I thought should block\n> the series, so I'm happy to leave\n> \n>   Reviewed-by: Glen Choo <chooglen@google.com>\n\nThanks a lot for your reviews!\n\nPatrick\n"},{"id":"477259","messageId":"20230513165925.GA113985@coredump.intra.peff.net","threadId":"59619","inReplyTo":"2cc7318697ef6062f6deadf7a22feb26c2c829e8.1683721293.git.ps@pks.im","subject":"Re: [PATCH v5 4/9] fetch: print left-hand side when fetching HEAD:foo","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2023-05-13T16:59:25Z","receivedAt":"2023-05-13T16:59:31Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, May 10, 2023 at 02:34:15PM +0200, Patrick Steinhardt wrote:\n\n> @@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n>  \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n>  \t\tif (verbosity > 0)\n>  \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n> -\t\t\t\t\t   remote, ref->name, summary_width);\n> +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n>  \t\treturn 0;\n>  \t}\n\nHere (and in other hunks) we now dereference remote_ref unconditionally.\nBut in existing parts of the code, we guard against remote_ref being\nNULL. E.g., later on:\n\n          if (!current || !updated) {\n                  const char *msg;\n                  const char *what;\n                  int r;\n                  /*\n                   * Nicely describe the new ref we're fetching.\n                   * Base this on the remote's ref name, as it's\n                   * more likely to follow a standard layout.\n                   */\n                  const char *name = remote_ref ? remote_ref->name : \"\";\n\t\t  [...]\n\nI'm not sure if the old code was being overly defensive, or if the new\ncode is ripe for a segfault. But it's probably worth looking into (it\nwas noticed by Coverity).\n\nLooking at the caller, it is always store_update_refs() which passes its\nown \"rm\", a pointer iterating over ref_map. And it should always be\nnon-NULL, since that's the loop condition.\n\nSo I think your code is fine, but you might want to double-check my\nlogic. (And it may be worth cleaning up the existing redundant check to\nprevent confusion).\n\n-Peff\n"},{"id":"477271","messageId":"ZGG_-7eD9TDanpjP@ncase","threadId":"59619","inReplyTo":"20230513165925.GA113985@coredump.intra.peff.net","subject":"Re: [PATCH v5 4/9] fetch: print left-hand side when fetching HEAD:foo","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2023-05-15T05:15:39Z","receivedAt":"2023-05-15T05:15:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, May 13, 2023 at 12:59:25PM -0400, Jeff King wrote:\n> On Wed, May 10, 2023 at 02:34:15PM +0200, Patrick Steinhardt wrote:\n> \n> > @@ -946,7 +948,7 @@ static int update_local_ref(struct ref *ref,\n> >  \tif (oideq(&ref->old_oid, &ref->new_oid)) {\n> >  \t\tif (verbosity > 0)\n> >  \t\t\tdisplay_ref_update(display_state, '=', _(\"[up to date]\"), NULL,\n> > -\t\t\t\t\t   remote, ref->name, summary_width);\n> > +\t\t\t\t\t   remote_ref->name, ref->name, summary_width);\n> >  \t\treturn 0;\n> >  \t}\n> \n> Here (and in other hunks) we now dereference remote_ref unconditionally.\n> But in existing parts of the code, we guard against remote_ref being\n> NULL. E.g., later on:\n> \n>           if (!current || !updated) {\n>                   const char *msg;\n>                   const char *what;\n>                   int r;\n>                   /*\n>                    * Nicely describe the new ref we're fetching.\n>                    * Base this on the remote's ref name, as it's\n>                    * more likely to follow a standard layout.\n>                    */\n>                   const char *name = remote_ref ? remote_ref->name : \"\";\n> \t\t  [...]\n> \n> I'm not sure if the old code was being overly defensive, or if the new\n> code is ripe for a segfault. But it's probably worth looking into (it\n> was noticed by Coverity).\n> \n> Looking at the caller, it is always store_update_refs() which passes its\n> own \"rm\", a pointer iterating over ref_map. And it should always be\n> non-NULL, since that's the loop condition.\n> \n> So I think your code is fine, but you might want to double-check my\n> logic. (And it may be worth cleaning up the existing redundant check to\n> prevent confusion).\n\nI really think that the code is overly defensive. As you mention, there\nis a single caller of `update_local_ref()`, only, and that caller\nalready dereferences the remote reference unconditionally anyway. So if\nthere was any way for `rm` to become `NULL` we'd already have a segfault\nearlier than in `update_local_ref()`.\n\nI'll send a follow-up patch series after the dust has settled that\nremoves the check.\n\nThanks!\n\nPatrick\n"}]}