{"thread":{"id":"62566","subject":"[PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","startedAt":"2024-11-29T12:01:30Z","lastAt":"2025-02-06T20:26:17Z","messageCount":57,"participants":["Toon Claes","Patrick Steinhardt","Jeff King","Michal Suchánek","Junio C Hamano","Jean-Noël AVILA"],"isPatch":true,"patchVersion":2,"patchTotal":null},"messages":[{"id":"508326","messageId":"20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com","threadId":"62566","inReplyTo":null,"subject":"[PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2024-11-29T12:00:42Z","receivedAt":"2024-11-29T12:01:30Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a raw commit hash. This enables the user to\nclone and checkout any revision they want. `--revision` can be used in\nconjunction with `--depth` to do a minimal clone that only contains the\nsources for a single revision. This can be useful for automated tests.\n\nThis type of shallow clone could also be achieved with the following set\nof commands:\n\n    git init the-repo\n    cd ./the-repo\n    git remote add origin <url>\n    git fetch --depth=1 origin <commit-id>\n    git checkout <commit-id>\n\nUnfortunately, this approach uses git-fetch(1) instead of git-clone(1),\nand only on git-clone(1) the bundle URIs advertised by the server are\nused. By adding this option `--revision` to git-clone(1) allows us to\nget the same end result, while benefiting from bundle URIs if advertised\nby the server.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\nI've submitted a similar change before[1], but this is a little bit of\nan alternative approach. There was a request[2] to also make it possible\nto clone a revision by a commit hash, so I've reworked the patch to\nenable that. Because of this I've renamed the option from `--ref` to\n`--revision`.\n\nIn the previous patch the code was heavily intertwined with handling\n`--single-branch`, `--branch`, and `--no-tags`. This make the code a lot\nmore complicated. In this version of the patch I've made the new option\nincompatible with those and added a separate code path to deal with it\nspecifically.\n\n[1]: https://lore.kernel.org/git/20240927085438.1010431-1-toon@iotcl.com/\n[2]: https://lore.kernel.org/git/ZzNJGHMlxGQyFV_c@kitsune.suse.cz/\n---\n Documentation/git-clone.txt |  7 ++++\n builtin/clone.c             | 64 +++++++++++++++++++++++++-----\n t/t5620-clone-revision.sh   | 97 +++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 158 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex 7acb4cb17618c6cbee5d6ebe41a53be03ebfaa6c..2a3f6d9deae4b817db50d8c4e555a6f33b8296f1 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -218,6 +218,13 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision` _<rev>_::\n+\tThis clones the given revision, and that revision only. The argument can\n+\tbe a symbolic ref name (e.g. `refs/heads/main`), or a raw commit hash.\n+\tUnless the revision points to a branch (i.e. ref starting with\n+\t`refs/heads/`), the HEAD is detached.\n+\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex c0013c75cdc7672abced893c36d2aaf4ff246045..0b118388e059068e78ac1eff4c0a9685a1c4e26b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -69,6 +69,7 @@ static char *option_template, *option_depth, *option_since;\n static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n+static char *option_rev = NULL;\n static struct string_list option_not = STRING_LIST_INIT_NODUP;\n static const char *real_git_dir;\n static const char *ref_format;\n@@ -141,6 +142,8 @@ static struct option builtin_clone_options[] = {\n \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t   N_(\"clone single revision <rev> and check out\")),\n \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t   N_(\"path to git-upload-pack on the remote\")),\n \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -682,12 +685,14 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n \t\t}\n \t} else if (our) {\n-\t\tstruct commit *c = lookup_commit_reference(the_repository,\n-\t\t\t\t\t\t\t   &our->old_oid);\n-\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n-\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n-\t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n-\t\t\t\tUPDATE_REFS_DIE_ON_ERR);\n+\t\tstruct commit *c =\n+\t\t\tlookup_commit_reference(the_repository, &our->old_oid);\n+\n+\t\tif (c)\n+\t\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n+\t\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n+\t\t\t\t\t\"HEAD\", &c->object.oid, NULL,\n+\t\t\t\t\tREF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);\n \t} else if (remote) {\n \t\t/*\n \t\t * We know remote HEAD points to a non-branch, or\n@@ -889,7 +894,7 @@ static void write_refspec_config(const char *src_ref_prefix,\n \tstruct strbuf key = STRBUF_INIT;\n \tstruct strbuf value = STRBUF_INIT;\n \n-\tif (option_mirror || !option_bare) {\n+\tif (!option_rev && (option_mirror || !option_bare)) {\n \t\tif (option_single_branch && !option_mirror) {\n \t\t\tif (option_branch) {\n \t\t\t\tif (starts_with(our_head_points_at->name, \"refs/tags/\"))\n@@ -968,7 +973,7 @@ int cmd_clone(int argc,\n \tchar *repo_to_free = NULL;\n \tchar *path = NULL, *dir, *display_repo = NULL;\n \tint dest_exists, real_dest_exists = 0;\n-\tconst struct ref *refs, *remote_head;\n+\tconst struct ref *refs = NULL, *remote_head;\n \tstruct ref *remote_head_points_at = NULL;\n \tconst struct ref *our_head_points_at;\n \tchar *unborn_head = NULL;\n@@ -1345,6 +1350,18 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tif (option_rev) {\n+\t\tif (option_branch)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--revision\", \"--branch\");\n+\t\tif (option_mirror)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--revision\", \"--mirror\");\n+\t\tif (option_bare)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t    \"--revision\", \"--bare\");\n+\t}\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1387,7 +1404,27 @@ int cmd_clone(int argc,\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n-\trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n+\tif (option_rev) {\n+\t\trefspec_clear(&remote->fetch);\n+\t\trefspec_init(&remote->fetch, REFSPEC_FETCH);\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\n+\t\tstrvec_clear(&transport_ls_refs_options.ref_prefixes);\n+\t\trefspec_ref_prefixes(&remote->fetch,\n+\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n+\n+\t\tif (transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\t\t/*\n+\t\t\t * We need to talk to the server to determine the hash\n+\t\t\t * algorithm, but when no ref prefixes are set the\n+\t\t\t * server announces all known refs, so ask the server to\n+\t\t\t * only tell us about HEAD.\n+\t\t\t */\n+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\t}\n+\n+\trefs = transport_get_remote_refs(transport,\n+\t\t\t\t\t &transport_ls_refs_options);\n \n \t/*\n \t * Now that we know what algorithm the remote side is using, let's set\n@@ -1461,8 +1498,13 @@ int cmd_clone(int argc,\n \t\t}\n \t}\n \n-\tif (refs)\n+\tif (option_rev) {\n+\t\tstruct ref **tail = &mapped_refs;\n+\n+\t\tget_fetch_map(refs, &remote->fetch.items[0], &tail, 1);\n+\t} else if (refs) {\n \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t}\n \n \tif (mapped_refs) {\n \t\t/*\n@@ -1495,6 +1537,8 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\ndiff --git a/t/t5620-clone-revision.sh b/t/t5620-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..833438e46d1c648ca46cf9d0a2948aad261c7f89\n--- /dev/null\n+++ b/t/t5620-clone-revision.sh\n@@ -0,0 +1,97 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\tgit for-each-ref refs/heads/feature > expect &&\n+\tgit -C dst for-each-ref > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0 >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst 2>err &&\n+\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\tgit clone --revision=$oid . dst 2>err &&\n+\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_expect_success 'clone with --revision and --bare fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --bare . dst\n+'\n+\n+test_done\n\n---\nbase-commit: cc01bad4a9f566cf4453c7edd6b433851b0835e2\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nBest regards,\n-- \nToon Claes <toon@iotcl.com>\n\n"},{"id":"508452","messageId":"Z02_YwCgHp_MVj3M@pks.im","threadId":"62566","inReplyTo":"20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com","subject":"Re: [PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-12-02T14:08:35Z","receivedAt":"2024-12-02T14:08:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Nov 29, 2024 at 01:00:42PM +0100, Toon Claes wrote:\n> The git-clone(1) command has the option `--branch` that allows the user\n> to select the branch they want HEAD to point to. In a non-bare\n> repository this also checks out that branch.\n> \n> Option `--branch` also accepts a tag. When a tag name is provided, the\n> commit this tag points to is checked out and HEAD is detached. Thus\n> `--branch` can be used to clone a repository and check out a ref kept\n> under `refs/heads` or `refs/tags`. But some other refs might be in use\n> as well. For example Git forges might use refs like `refs/pull/<id>` and\n> `refs/merge-requests/<id>` to track pull/merge requests. These refs\n> cannot selected upon git-clone(1).\n\ns/cannot/cannot be/\n\n> Add option `--revision` to git-clone(1). This option accepts a fully\n> qualified reference, or a raw commit hash. This enables the user to\n> clone and checkout any revision they want. `--revision` can be used in\n\ns/checkout/check out/\n\nDoes this have to be a raw commit hash, or do we also accept an\narbitrary committish like a tag that peels down to a commit?\n\n> conjunction with `--depth` to do a minimal clone that only contains the\n> sources for a single revision. This can be useful for automated tests.\n\nIt's implicity, but automated tests in this context probably means CI\nsystems.\n\n> This type of shallow clone could also be achieved with the following set\n> of commands:\n> \n>     git init the-repo\n>     cd ./the-repo\n>     git remote add origin <url>\n>     git fetch --depth=1 origin <commit-id>\n>     git checkout <commit-id>\n> \n> Unfortunately, this approach uses git-fetch(1) instead of git-clone(1),\n> and only on git-clone(1) the bundle URIs advertised by the server are\n> used. By adding this option `--revision` to git-clone(1) allows us to\n> get the same end result, while benefiting from bundle URIs if advertised\n> by the server.\n\nI'd claim that this is not only about enabling bundle URIs, but also\nabout making this easier to work with in the first place. The above is\nrather on the complex side even though the use case at hand is not all\nthat esoteric.\n\n> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> index 7acb4cb17618c6cbee5d6ebe41a53be03ebfaa6c..2a3f6d9deae4b817db50d8c4e555a6f33b8296f1 100644\n> --- a/Documentation/git-clone.txt\n> +++ b/Documentation/git-clone.txt\n> @@ -218,6 +218,13 @@ objects from the source repository into a pack in the cloned repository.\n>  \t`--branch` can also take tags and detaches the `HEAD` at that commit\n>  \tin the resulting repository.\n>  \n> +`--revision` _<rev>_::\n> +\tThis clones the given revision, and that revision only. The argument can\n> +\tbe a symbolic ref name (e.g. `refs/heads/main`), or a raw commit hash.\n\nNit: while I know what you want to say with \"symbolic ref name\", I think\nit's a bit awkwardly worded because a reader may confuse it with an\nactual symbolic ref. I would just drop the \"symbolic\".\n\nWe should also clarify whether this accepts tags that peel down to a\ncommit.\n\n> +\tUnless the revision points to a branch (i.e. ref starting with\n> +\t`refs/heads/`), the HEAD is detached.\n\nOkay, makes sense. When cloning a tag or committish we wouldn't know how\nto name the branch, so we instead put us into detached HEAD mode.\n\nI could see an argument that we should do this unconditionally even when\ncloning a branch to simplify the UX by always doing the same thing, no\nmatter what the user has passed.\n\n> +\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n\nThe restriction on `--branch` and `--mirror` is sensible: we ask for a\nsingle commit, so neither of those options would make sense when taken\ntogether.\n\nBut why can't we pass `--bare`? That only controls whether we have a\nworking tree or not, and a bare repository can have a detached HEAD, as\nwell.\n\n>  `-u` _<upload-pack>_::\n>  `--upload-pack` _<upload-pack>_::\n>  \tWhen given, and the repository to clone from is accessed\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index c0013c75cdc7672abced893c36d2aaf4ff246045..0b118388e059068e78ac1eff4c0a9685a1c4e26b 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -69,6 +69,7 @@ static char *option_template, *option_depth, *option_since;\n>  static char *option_origin = NULL;\n>  static char *remote_name = NULL;\n>  static char *option_branch = NULL;\n> +static char *option_rev = NULL;\n>  static struct string_list option_not = STRING_LIST_INIT_NODUP;\n>  static const char *real_git_dir;\n>  static const char *ref_format;\n> @@ -141,6 +142,8 @@ static struct option builtin_clone_options[] = {\n>  \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n>  \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n>  \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n> +\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n> +\t\t   N_(\"clone single revision <rev> and check out\")),\n>  \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n>  \t\t   N_(\"path to git-upload-pack on the remote\")),\n>  \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n> @@ -682,12 +685,14 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>  \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n>  \t\t}\n>  \t} else if (our) {\n> -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n> -\t\t\t\t\t\t\t   &our->old_oid);\n> -\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n> -\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n> -\t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n> -\t\t\t\tUPDATE_REFS_DIE_ON_ERR);\n> +\t\tstruct commit *c =\n> +\t\t\tlookup_commit_reference(the_repository, &our->old_oid);\n> +\n> +\t\tif (c)\n> +\t\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n> +\t\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n> +\t\t\t\t\t\"HEAD\", &c->object.oid, NULL,\n> +\t\t\t\t\tREF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);\n\nThis change is a bit surprising. Why can it happen that we don't have\nthe commit now?\n\n>  \t} else if (remote) {\n>  \t\t/*\n>  \t\t * We know remote HEAD points to a non-branch, or\n> @@ -889,7 +894,7 @@ static void write_refspec_config(const char *src_ref_prefix,\n>  \tstruct strbuf key = STRBUF_INIT;\n>  \tstruct strbuf value = STRBUF_INIT;\n>  \n> -\tif (option_mirror || !option_bare) {\n> +\tif (!option_rev && (option_mirror || !option_bare)) {\n>  \t\tif (option_single_branch && !option_mirror) {\n>  \t\t\tif (option_branch) {\n>  \t\t\t\tif (starts_with(our_head_points_at->name, \"refs/tags/\"))\n> @@ -968,7 +973,7 @@ int cmd_clone(int argc,\n>  \tchar *repo_to_free = NULL;\n>  \tchar *path = NULL, *dir, *display_repo = NULL;\n>  \tint dest_exists, real_dest_exists = 0;\n> -\tconst struct ref *refs, *remote_head;\n> +\tconst struct ref *refs = NULL, *remote_head;\n>  \tstruct ref *remote_head_points_at = NULL;\n>  \tconst struct ref *our_head_points_at;\n>  \tchar *unborn_head = NULL;\n> @@ -1345,6 +1350,18 @@ int cmd_clone(int argc,\n>  \n>  \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n>  \n> +\tif (option_rev) {\n> +\t\tif (option_branch)\n> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n> +\t\t\t    \"--revision\", \"--branch\");\n> +\t\tif (option_mirror)\n> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n> +\t\t\t    \"--revision\", \"--mirror\");\n> +\t\tif (option_bare)\n> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n> +\t\t\t    \"--revision\", \"--bare\");\n> +\t}\n> +\n>  \tif (reject_shallow)\n>  \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n>  \tif (option_depth)\n\nWe have `die_for_incompatible_opt{3,4}()`, but don't yet have the\neqiuvalent for two options. I know this has come up repeatedly during\nreviews. Maybe we should finally add that helper in a separate commit?\n\n> @@ -1387,7 +1404,27 @@ int cmd_clone(int argc,\n>  \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n>  \t\t\t    \"refs/tags/\");\n>  \n> -\trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n> +\tif (option_rev) {\n> +\t\trefspec_clear(&remote->fetch);\n\nWhy do we have to clear the fetchspec first? I would have expected us to\nskip over any previous steps that may have populated it when given the\n\"--revision=\" option.\n\n> +\t\trefspec_init(&remote->fetch, REFSPEC_FETCH);\n> +\t\trefspec_append(&remote->fetch, option_rev);\n> +\n> +\t\tstrvec_clear(&transport_ls_refs_options.ref_prefixes);\n> +\t\trefspec_ref_prefixes(&remote->fetch,\n> +\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n> +\n> +\t\tif (transport_ls_refs_options.ref_prefixes.nr == 0)\n> +\t\t\t/*\n> +\t\t\t * We need to talk to the server to determine the hash\n> +\t\t\t * algorithm, but when no ref prefixes are set the\n> +\t\t\t * server announces all known refs, so ask the server to\n> +\t\t\t * only tell us about HEAD.\n> +\t\t\t */\n> +\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n> +\t}\n> +\n> +\trefs = transport_get_remote_refs(transport,\n> +\t\t\t\t\t &transport_ls_refs_options);\n>  \n>  \t/*\n>  \t * Now that we know what algorithm the remote side is using, let's set\n> @@ -1461,8 +1498,13 @@ int cmd_clone(int argc,\n>  \t\t}\n>  \t}\n>  \n> -\tif (refs)\n> +\tif (option_rev) {\n> +\t\tstruct ref **tail = &mapped_refs;\n> +\n> +\t\tget_fetch_map(refs, &remote->fetch.items[0], &tail, 1);\n> +\t} else if (refs) {\n>  \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n> +\t}\n>  \n>  \tif (mapped_refs) {\n>  \t\t/*\n> @@ -1495,6 +1537,8 @@ int cmd_clone(int argc,\n>  \t\tif (!our_head_points_at)\n>  \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n>  \t\t\t    option_branch, remote_name);\n> +\t} else if (option_rev) {\n> +\t\tour_head_points_at = mapped_refs;\n>  \t} else if (remote_head_points_at) {\n>  \t\tour_head_points_at = remote_head_points_at;\n>  \t} else if (remote_head) {\n> diff --git a/t/t5620-clone-revision.sh b/t/t5620-clone-revision.sh\n> new file mode 100755\n> index 0000000000000000000000000000000000000000..833438e46d1c648ca46cf9d0a2948aad261c7f89\n> --- /dev/null\n> +++ b/t/t5620-clone-revision.sh\n> @@ -0,0 +1,97 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git clone --revision'\n> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> +\n> +TEST_PASSES_SANITIZE_LEAK=true\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' '\n> +\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n> +\ttest_commit \"second commit\" README \"Hello world\" v1.0 &&\n> +\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n> +\tgit switch -c feature v1.0 &&\n> +\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n> +\tgit switch main\n> +'\n> +\n> +test_expect_success 'clone with --revision being a branch' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=refs/heads/feature . dst &&\n> +\tgit rev-parse refs/heads/feature >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual &&\n> +\tgit for-each-ref refs/heads/feature > expect &&\n> +\tgit -C dst for-each-ref > actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'clone with --depth and --revision being a branch' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n> +\tgit rev-parse refs/heads/feature >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'clone with --revision being a tag' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=refs/tags/v1.0 . dst &&\n> +\tgit rev-parse refs/tags/v1.0 >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nIs this an annotated tag or a normal one?\n\n> +test_expect_success 'clone with --revision being HEAD' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=HEAD . dst &&\n> +\tgit rev-parse HEAD >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nAh, interesting. So we can essentially clone whatever the remote HEAD\npoints to. Nice.\n\n> +test_expect_success 'clone with --revision being a raw commit hash' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\toid=$(git rev-parse refs/heads/feature) &&\n> +\tgit clone --revision=$oid . dst &&\n> +\techo $oid >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'clone with --revision being a short raw commit hash' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\toid=$(git rev-parse --short refs/heads/feature) &&\n> +\tgit clone --revision=$oid . dst 2>err &&\n> +\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n> +'\n\nYou probably want to use `test_grep` for these.\n\nWhat makes me wonder though is that these clones don't fail, but\ngenerate a warning because we don't understand the ref. I would have\nexpected us to either bail out completely or succeed and end up with a\nwell-formed repository that has the commit in question checked out.\n\n> +test_expect_success 'clone with --revision being a tree hash' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n> +\tgit clone --revision=$oid . dst 2>err &&\n> +\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n> +'\n> +\n> +test_expect_success 'clone with --revision being the parent of a ref fails' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n> +'\n\nIt might make sense to also `test_grep` these error messages so that we\nknow to not fail due to unrelated reasons.\n\nThanks!\n\nPatrick\n"},{"id":"508470","messageId":"20241202211715.GA787456@coredump.intra.peff.net","threadId":"62566","inReplyTo":"Z02_YwCgHp_MVj3M@pks.im","subject":"Re: [PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-12-02T21:17:15Z","receivedAt":"2024-12-02T21:17:17Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Dec 02, 2024 at 03:08:35PM +0100, Patrick Steinhardt wrote:\n\n> > Add option `--revision` to git-clone(1). This option accepts a fully\n> > qualified reference, or a raw commit hash. This enables the user to\n> > clone and checkout any revision they want. `--revision` can be used in\n> \n> s/checkout/check out/\n> \n> Does this have to be a raw commit hash, or do we also accept an\n> arbitrary committish like a tag that peels down to a commit?\n\nI don't think anything except a raw hash will be possible without a\nprotocol extension. upload-pack accepts only a full object id on its\n\"want\" lines, so the client needs to either start with one, or start\nwith a refname that it can convert to one from the advertisement.\n\n(There's also the \"want-ref\" extension, but it only takes full\nrefnames).\n\n-Peff\n"},{"id":"508542","messageId":"Z08k6hANEQVe2g70@kitsune.suse.cz","threadId":"62566","inReplyTo":"Z02_YwCgHp_MVj3M@pks.im","subject":"Re: [PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Michal Suchánek","fromEmail":"msuchanek@suse.de","sentAt":"2024-12-03T15:34:02Z","receivedAt":"2024-12-03T15:34:05Z","isPatch":true,"sender":{"key":"msuchanek@suse.de","avatar":"https://avatars.githubusercontent.com/u/787652?v=4"},"body":"Hello\n\nOn Mon, Dec 02, 2024 at 03:08:35PM +0100, Patrick Steinhardt wrote:\n> On Fri, Nov 29, 2024 at 01:00:42PM +0100, Toon Claes wrote:\n> > The git-clone(1) command has the option `--branch` that allows the user\n> > to select the branch they want HEAD to point to. In a non-bare\n> > repository this also checks out that branch.\n> > \n> > Option `--branch` also accepts a tag. When a tag name is provided, the\n> > commit this tag points to is checked out and HEAD is detached. Thus\n> > `--branch` can be used to clone a repository and check out a ref kept\n> > under `refs/heads` or `refs/tags`. But some other refs might be in use\n> > as well. For example Git forges might use refs like `refs/pull/<id>` and\n> > `refs/merge-requests/<id>` to track pull/merge requests. These refs\n> > cannot selected upon git-clone(1).\n> \n> s/cannot/cannot be/\n> \n> > Add option `--revision` to git-clone(1). This option accepts a fully\n> > qualified reference, or a raw commit hash. This enables the user to\n> > clone and checkout any revision they want. `--revision` can be used in\n> \n> s/checkout/check out/\n> \n> Does this have to be a raw commit hash, or do we also accept an\n> arbitrary committish like a tag that peels down to a commit?\n> \n> > conjunction with `--depth` to do a minimal clone that only contains the\n> > sources for a single revision. This can be useful for automated tests.\n> \n> It's implicity, but automated tests in this context probably means CI\n> systems.\n> \n> > This type of shallow clone could also be achieved with the following set\n> > of commands:\n> > \n> >     git init the-repo\n> >     cd ./the-repo\n> >     git remote add origin <url>\n> >     git fetch --depth=1 origin <commit-id>\n> >     git checkout <commit-id>\n> > \n> > Unfortunately, this approach uses git-fetch(1) instead of git-clone(1),\n> > and only on git-clone(1) the bundle URIs advertised by the server are\n> > used. By adding this option `--revision` to git-clone(1) allows us to\n> > get the same end result, while benefiting from bundle URIs if advertised\n> > by the server.\n> \n> I'd claim that this is not only about enabling bundle URIs, but also\n> about making this easier to work with in the first place. The above is\n> rather on the complex side even though the use case at hand is not all\n> that esoteric.\n> \n> > diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> > index 7acb4cb17618c6cbee5d6ebe41a53be03ebfaa6c..2a3f6d9deae4b817db50d8c4e555a6f33b8296f1 100644\n> > --- a/Documentation/git-clone.txt\n> > +++ b/Documentation/git-clone.txt\n> > @@ -218,6 +218,13 @@ objects from the source repository into a pack in the cloned repository.\n> >  \t`--branch` can also take tags and detaches the `HEAD` at that commit\n> >  \tin the resulting repository.\n> >  \n> > +`--revision` _<rev>_::\n> > +\tThis clones the given revision, and that revision only. The argument can\n> > +\tbe a symbolic ref name (e.g. `refs/heads/main`), or a raw commit hash.\n> \n> Nit: while I know what you want to say with \"symbolic ref name\", I think\n> it's a bit awkwardly worded because a reader may confuse it with an\n> actual symbolic ref. I would just drop the \"symbolic\".\n> \n> We should also clarify whether this accepts tags that peel down to a\n> commit.\n> \n> > +\tUnless the revision points to a branch (i.e. ref starting with\n> > +\t`refs/heads/`), the HEAD is detached.\n> \n> Okay, makes sense. When cloning a tag or committish we wouldn't know how\n> to name the branch, so we instead put us into detached HEAD mode.\n> \n> I could see an argument that we should do this unconditionally even when\n> cloning a branch to simplify the UX by always doing the same thing, no\n> matter what the user has passed.\n\nI don't think that's expected, most commands that take a committish\nwould create a detached head only when it cannot be attached or when\nspecifically instructed, see eg.\n\nusage: git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n                        [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\n\n\n       git checkout [-q] [-f] [-m] [<branch>]\n       git checkout [-q] [-f] [-m] --detach [<branch>]\n       git checkout [-q] [-f] [-m] [--detach] <commit>\n\nThanks\n\nMichal\n"},{"id":"509332","messageId":"87ttazhq6l.fsf@iotcl.com","threadId":"62566","inReplyTo":"Z02_YwCgHp_MVj3M@pks.im","subject":"Re: [PATCH v2] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2024-12-19T11:23:30Z","receivedAt":"2024-12-19T11:23:47Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> conjunction with `--depth` to do a minimal clone that only contains the\n>> sources for a single revision. This can be useful for automated tests.\n>\n> It's implicity, but automated tests in this context probably means CI\n> systems.\n\nI wasn't sure which term is more commonly used in commit messages here.\nI know some people consider CI to be a completely different thing from\n\"automated testing\", that's why I didn't use \"CI\". I'll modify the\nmessage to be more explicit and say something like:\n\n    This can be useful for automated tests running in CI systems.\n\n>> This type of shallow clone could also be achieved with the following set\n>> of commands:\n>> \n>>     git init the-repo\n>>     cd ./the-repo\n>>     git remote add origin <url>\n>>     git fetch --depth=1 origin <commit-id>\n>>     git checkout <commit-id>\n>> \n>> Unfortunately, this approach uses git-fetch(1) instead of git-clone(1),\n>> and only on git-clone(1) the bundle URIs advertised by the server are\n>> used. By adding this option `--revision` to git-clone(1) allows us to\n>> get the same end result, while benefiting from bundle URIs if advertised\n>> by the server.\n>\n> I'd claim that this is not only about enabling bundle URIs, but also\n> about making this easier to work with in the first place. The above is\n> rather on the complex side even though the use case at hand is not all\n> that esoteric.\n\nOkay, that's fair. I'll add that to the commit message as well.\n\n>> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n>> index 7acb4cb17618c6cbee5d6ebe41a53be03ebfaa6c..2a3f6d9deae4b817db50d8c4e555a6f33b8296f1 100644\n>> --- a/Documentation/git-clone.txt\n>> +++ b/Documentation/git-clone.txt\n>> @@ -218,6 +218,13 @@ objects from the source repository into a pack in the cloned repository.\n>>  \t`--branch` can also take tags and detaches the `HEAD` at that commit\n>>  \tin the resulting repository.\n>>  \n>> +`--revision` _<rev>_::\n>> +\tThis clones the given revision, and that revision only. The argument can\n>> +\tbe a symbolic ref name (e.g. `refs/heads/main`), or a raw commit hash.\n>\n> Nit: while I know what you want to say with \"symbolic ref name\", I think\n> it's a bit awkwardly worded because a reader may confuse it with an\n> actual symbolic ref. I would just drop the \"symbolic\".\n>\n> We should also clarify whether this accepts tags that peel down to a\n> commit.\n\nMakes sense, I'll modify this piece of documentation a bit.\n\n>> +\tUnless the revision points to a branch (i.e. ref starting with\n>> +\t`refs/heads/`), the HEAD is detached.\n>\n> Okay, makes sense. When cloning a tag or committish we wouldn't know how\n> to name the branch, so we instead put us into detached HEAD mode.\n>\n> I could see an argument that we should do this unconditionally even when\n> cloning a branch to simplify the UX by always doing the same thing, no\n> matter what the user has passed.\n\nI've been thinking about this as well, but I didn't see any benefit to\nthe user in making branches detach as well. The existing option\n`--branch` detaches only when it resolves to a tag, so we would have\nsimilar behavior. Plus, I would have to add extra code to detach a\nbranch, so I left it out and would wait for feedback from review here\nwhether detaching a branch would be better.\n\n>> +\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n>\n> The restriction on `--branch` and `--mirror` is sensible: we ask for a\n> single commit, so neither of those options would make sense when taken\n> together.\n>\n> But why can't we pass `--bare`? That only controls whether we have a\n> working tree or not, and a bare repository can have a detached HEAD, as\n> well.\n\nHah, good point. It felt to me it was incompatible, but after some\ntesting it *is* compatible. I'll remove the limitation.\n\n>>  `-u` _<upload-pack>_::\n>>  `--upload-pack` _<upload-pack>_::\n>>  \tWhen given, and the repository to clone from is accessed\n>> diff --git a/builtin/clone.c b/builtin/clone.c\n>> index c0013c75cdc7672abced893c36d2aaf4ff246045..0b118388e059068e78ac1eff4c0a9685a1c4e26b 100644\n>> --- a/builtin/clone.c\n>> +++ b/builtin/clone.c\n>> @@ -69,6 +69,7 @@ static char *option_template, *option_depth, *option_since;\n>>  static char *option_origin = NULL;\n>>  static char *remote_name = NULL;\n>>  static char *option_branch = NULL;\n>> +static char *option_rev = NULL;\n>>  static struct string_list option_not = STRING_LIST_INIT_NODUP;\n>>  static const char *real_git_dir;\n>>  static const char *ref_format;\n>> @@ -141,6 +142,8 @@ static struct option builtin_clone_options[] = {\n>>  \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n>>  \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n>>  \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n>> +\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n>> +\t\t   N_(\"clone single revision <rev> and check out\")),\n>>  \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n>>  \t\t   N_(\"path to git-upload-pack on the remote\")),\n>>  \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n>> @@ -682,12 +685,14 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>>  \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n>>  \t\t}\n>>  \t} else if (our) {\n>> -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n>> -\t\t\t\t\t\t\t   &our->old_oid);\n>> -\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n>> -\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n>> -\t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n>> -\t\t\t\tUPDATE_REFS_DIE_ON_ERR);\n>> +\t\tstruct commit *c =\n>> +\t\t\tlookup_commit_reference(the_repository, &our->old_oid);\n>> +\n>> +\t\tif (c)\n>> +\t\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n>> +\t\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n>> +\t\t\t\t\t\"HEAD\", &c->object.oid, NULL,\n>> +\t\t\t\t\tREF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);\n>\n> This change is a bit surprising. Why can it happen that we don't have\n> the commit now?\n\nFor the added test case (8) \"clone with --revision being a tree hash\" we\nwould hit a segmentation fault without this fix. So when the user would\npass a object ID that's not a commit, we'd hit this code.\n\nBut I've realized we end up with `c` being `NULL` also in case a tree\noid was passed to `--revision`. This is what happens in that case:\n\n    Cloning into 'dst'...\n    done.\n    error: object be0788944df13c5d170e050f2fe178360c3df5a5 is a tree, not a commit\n    warning: remote HEAD refers to nonexistent ref, unable to checkout\n\nThis is not correct. We should die hard in that situation. I'm fixing\nthat. \n\n>>  \t} else if (remote) {\n>>  \t\t/*\n>>  \t\t * We know remote HEAD points to a non-branch, or\n>> @@ -889,7 +894,7 @@ static void write_refspec_config(const char *src_ref_prefix,\n>>  \tstruct strbuf key = STRBUF_INIT;\n>>  \tstruct strbuf value = STRBUF_INIT;\n>>  \n>> -\tif (option_mirror || !option_bare) {\n>> +\tif (!option_rev && (option_mirror || !option_bare)) {\n>>  \t\tif (option_single_branch && !option_mirror) {\n>>  \t\t\tif (option_branch) {\n>>  \t\t\t\tif (starts_with(our_head_points_at->name, \"refs/tags/\"))\n>> @@ -968,7 +973,7 @@ int cmd_clone(int argc,\n>>  \tchar *repo_to_free = NULL;\n>>  \tchar *path = NULL, *dir, *display_repo = NULL;\n>>  \tint dest_exists, real_dest_exists = 0;\n>> -\tconst struct ref *refs, *remote_head;\n>> +\tconst struct ref *refs = NULL, *remote_head;\n>>  \tstruct ref *remote_head_points_at = NULL;\n>>  \tconst struct ref *our_head_points_at;\n>>  \tchar *unborn_head = NULL;\n>> @@ -1345,6 +1350,18 @@ int cmd_clone(int argc,\n>>  \n>>  \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n>>  \n>> +\tif (option_rev) {\n>> +\t\tif (option_branch)\n>> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>> +\t\t\t    \"--revision\", \"--branch\");\n>> +\t\tif (option_mirror)\n>> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>> +\t\t\t    \"--revision\", \"--mirror\");\n>> +\t\tif (option_bare)\n>> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>> +\t\t\t    \"--revision\", \"--bare\");\n>> +\t}\n>> +\n>>  \tif (reject_shallow)\n>>  \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n>>  \tif (option_depth)\n>\n> We have `die_for_incompatible_opt{3,4}()`, but don't yet have the\n> eqiuvalent for two options. I know this has come up repeatedly during\n> reviews. Maybe we should finally add that helper in a separate commit?\n\nI didn't know about those. Although because it's only a few lines in\nparse-options.h, I'm including it in the same commit. But I can split up\nif you prefer? I didn't touch any of the existing code in clone.c that\nalso could use this function, or should I?\n\n>> @@ -1387,7 +1404,27 @@ int cmd_clone(int argc,\n>>  \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n>>  \t\t\t    \"refs/tags/\");\n>>  \n>> -\trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n>> +\tif (option_rev) {\n>> +\t\trefspec_clear(&remote->fetch);\n>\n> Why do we have to clear the fetchspec first? I would have expected us to\n> skip over any previous steps that may have populated it when given the\n> \"--revision=\" option.\n\nThere are various places where we add items to `remote->fetch` and\n`transport_ls_refs_options.ref_prefixes`. To me it felt way more\ncomplicated to wrap them in `if (!option_rev)`.\n\n>> +\t\trefspec_init(&remote->fetch, REFSPEC_FETCH);\n>> +\t\trefspec_append(&remote->fetch, option_rev);\n>> +\n>> +\t\tstrvec_clear(&transport_ls_refs_options.ref_prefixes);\n>> +\t\trefspec_ref_prefixes(&remote->fetch,\n>> +\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n>> +\n>> +\t\tif (transport_ls_refs_options.ref_prefixes.nr == 0)\n>> +\t\t\t/*\n>> +\t\t\t * We need to talk to the server to determine the hash\n>> +\t\t\t * algorithm, but when no ref prefixes are set the\n>> +\t\t\t * server announces all known refs, so ask the server to\n>> +\t\t\t * only tell us about HEAD.\n>> +\t\t\t */\n>> +\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n>> +\t}\n>> +\n>> +\trefs = transport_get_remote_refs(transport,\n>> +\t\t\t\t\t &transport_ls_refs_options);\n>>  \n>>  \t/*\n>>  \t * Now that we know what algorithm the remote side is using, let's set\n>> @@ -1461,8 +1498,13 @@ int cmd_clone(int argc,\n>>  \t\t}\n>>  \t}\n>>  \n>> -\tif (refs)\n>> +\tif (option_rev) {\n>> +\t\tstruct ref **tail = &mapped_refs;\n>> +\n>> +\t\tget_fetch_map(refs, &remote->fetch.items[0], &tail, 1);\n>> +\t} else if (refs) {\n>>  \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n>> +\t}\n>>  \n>>  \tif (mapped_refs) {\n>>  \t\t/*\n>> @@ -1495,6 +1537,8 @@ int cmd_clone(int argc,\n>>  \t\tif (!our_head_points_at)\n>>  \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n>>  \t\t\t    option_branch, remote_name);\n>> +\t} else if (option_rev) {\n>> +\t\tour_head_points_at = mapped_refs;\n>>  \t} else if (remote_head_points_at) {\n>>  \t\tour_head_points_at = remote_head_points_at;\n>>  \t} else if (remote_head) {\n>> diff --git a/t/t5620-clone-revision.sh b/t/t5620-clone-revision.sh\n>> new file mode 100755\n>> index 0000000000000000000000000000000000000000..833438e46d1c648ca46cf9d0a2948aad261c7f89\n>> --- /dev/null\n>> +++ b/t/t5620-clone-revision.sh\n>> @@ -0,0 +1,97 @@\n>> +#!/bin/sh\n>> +\n>> +test_description='tests for git clone --revision'\n>> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>> +\n>> +TEST_PASSES_SANITIZE_LEAK=true\n>> +. ./test-lib.sh\n>> +\n>> +test_expect_success 'setup' '\n>> +\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n>> +\ttest_commit \"second commit\" README \"Hello world\" v1.0 &&\n>> +\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n>> +\tgit switch -c feature v1.0 &&\n>> +\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n>> +\tgit switch main\n>> +'\n>> +\n>> +test_expect_success 'clone with --revision being a branch' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\tgit clone --revision=refs/heads/feature . dst &&\n>> +\tgit rev-parse refs/heads/feature >expect &&\n>> +\tgit -C dst rev-parse HEAD >actual &&\n>> +\ttest_cmp expect actual &&\n>> +\tgit for-each-ref refs/heads/feature > expect &&\n>> +\tgit -C dst for-each-ref > actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'clone with --depth and --revision being a branch' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n>> +\tgit rev-parse refs/heads/feature >expect &&\n>> +\tgit -C dst rev-parse HEAD >actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'clone with --revision being a tag' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\tgit clone --revision=refs/tags/v1.0 . dst &&\n>> +\tgit rev-parse refs/tags/v1.0 >expect &&\n>> +\tgit -C dst rev-parse HEAD >actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>\n> Is this an annotated tag or a normal one?\n\nGood call, it was a lightweight tag. I'll change it into a annotated.\n\n>> +test_expect_success 'clone with --revision being HEAD' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\tgit clone --revision=HEAD . dst &&\n>> +\tgit rev-parse HEAD >expect &&\n>> +\tgit -C dst rev-parse HEAD >actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>\n> Ah, interesting. So we can essentially clone whatever the remote HEAD\n> points to. Nice.\n>\n>> +test_expect_success 'clone with --revision being a raw commit hash' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\toid=$(git rev-parse refs/heads/feature) &&\n>> +\tgit clone --revision=$oid . dst &&\n>> +\techo $oid >expect &&\n>> +\tgit -C dst rev-parse HEAD >actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'clone with --revision being a short raw commit hash' '\n>> +\ttest_when_finished \"rm -rf dst\" &&\n>> +\toid=$(git rev-parse --short refs/heads/feature) &&\n>> +\tgit clone --revision=$oid . dst 2>err &&\n>> +\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n>> +'\n>\n> You probably want to use `test_grep` for these.\n\nAh, TIL.\n\n> What makes me wonder though is that these clones don't fail, but\n> generate a warning because we don't understand the ref. I would have\n> expected us to either bail out completely or succeed and end up with a\n> well-formed repository that has the commit in question checked out.\n\nBecause this code path exists already, I considered it to be valid. But\nit's good you're making me aware of this, because if we were not able to\nresolve the given revision, we should die right there.\n\nThanks for all the feedback. The second version will be sent out soon.\n\n-- \nToon\n"},{"id":"509334","messageId":"20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com","threadId":"62566","inReplyTo":"20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com","subject":"[PATCH v3] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2024-12-19T11:58:50Z","receivedAt":"2024-12-19T11:59:54Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot be selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a raw commit hash. This enables the user to\nclone and check out any revision they want. `--revision` can be used in\nconjunction with `--depth` to do a minimal clone that only contains the\nsources for a single revision. This can be useful for automated tests\nrunning in CI systems.\n\nThis type of shallow clone could also be achieved with the following set\nof commands:\n\n    git init the-repo\n    cd ./the-repo\n    git remote add origin <url>\n    git fetch --depth=1 origin <commit-id>\n    git checkout <commit-id>\n\nAdding this new option to git-clone(1) simplifies this not uncommon\nuse-case. And besides simplifying this, it enables the use of\ngit-clone(1) over git-fetch(1). This is beneficial in case bundle URIs\nare available on the server. Bundle URIs are only used on clone, not on\nfetch, so using allowing the user to use git-clone(1) here makes them\nbenefit from bundle URIs if advertised by the server.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\nI've submitted a similar change before[1], but this is a little bit of\nan alternative approach. There was a request[2] to also make it possible\nto clone a revision by a commit hash, so I've reworked the patch to\nenable that. Because of this I've renamed the option from `--ref` to\n`--revision`.\n\nIn the previous patch the code was heavily intertwined with handling\n`--single-branch`, `--branch`, and `--no-tags`. This make the code a lot\nmore complicated. In this version of the patch I've made the new option\nincompatible with those and added a separate code path to deal with it\nspecifically.\n\n[1]: https://lore.kernel.org/git/20240927085438.1010431-1-toon@iotcl.com/\n[2]: https://lore.kernel.org/git/ZzNJGHMlxGQyFV_c@kitsune.suse.cz/\n---\nChanges in v3:\n- Fail early when the revision was not found on the remote, instead of\n  creating a clone that's in an invalid state.\n- State more clearly in the commit message adding this option is useful\n  for a not uncommon use-case.\n- Be explicit in the documentation the ref needs to peel down to a\n  commit.\n- Die in case we try to update_head() to an object that's not a commit.\n- Allow combining `--revision` with `--bare`.\n- Add die_for_incompatible_opt2() to parse-options.h and use it for the\n  options that are not compatible with the new `--revision` option.\n- Small tweaks to the added tests.\n- Small touchups on commit messages.\n- Link to v2: https://lore.kernel.org/r/20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com\n---\nRange-diff versus v2:\n\n1:  b5cf7362be ! 1:  592a22fdbd builtin/clone: teach git-clone(1) the --revision= option\n    @@ Commit message\n         under `refs/heads` or `refs/tags`. But some other refs might be in use\n         as well. For example Git forges might use refs like `refs/pull/<id>` and\n         `refs/merge-requests/<id>` to track pull/merge requests. These refs\n    -    cannot selected upon git-clone(1).\n    +    cannot be selected upon git-clone(1).\n     \n         Add option `--revision` to git-clone(1). This option accepts a fully\n         qualified reference, or a raw commit hash. This enables the user to\n    -    clone and checkout any revision they want. `--revision` can be used in\n    +    clone and check out any revision they want. `--revision` can be used in\n         conjunction with `--depth` to do a minimal clone that only contains the\n    -    sources for a single revision. This can be useful for automated tests.\n    +    sources for a single revision. This can be useful for automated tests\n    +    running in CI systems.\n     \n         This type of shallow clone could also be achieved with the following set\n         of commands:\n    @@ Commit message\n             git fetch --depth=1 origin <commit-id>\n             git checkout <commit-id>\n     \n    -    Unfortunately, this approach uses git-fetch(1) instead of git-clone(1),\n    -    and only on git-clone(1) the bundle URIs advertised by the server are\n    -    used. By adding this option `--revision` to git-clone(1) allows us to\n    -    get the same end result, while benefiting from bundle URIs if advertised\n    -    by the server.\n    +    Adding this new option to git-clone(1) simplifies this not uncommon\n    +    use-case. And besides simplifying this, it enables the use of\n    +    git-clone(1) over git-fetch(1). This is beneficial in case bundle URIs\n    +    are available on the server. Bundle URIs are only used on clone, not on\n    +    fetch, so using allowing the user to use git-clone(1) here makes them\n    +    benefit from bundle URIs if advertised by the server.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    @@ Documentation/git-clone.txt: objects from the source repository into a pack in t\n      \n     +`--revision` _<rev>_::\n     +\tThis clones the given revision, and that revision only. The argument can\n    -+\tbe a symbolic ref name (e.g. `refs/heads/main`), or a raw commit hash.\n    -+\tUnless the revision points to a branch (i.e. ref starting with\n    -+\t`refs/heads/`), the HEAD is detached.\n    ++\tbe a ref name (e.g. `refs/heads/main`) that peels down to a commit, or a\n    ++\traw commit hash.\n    ++\tThe given revision is checked out, and for any revision other than a\n    ++\tbranch (i.e. ref starting with `refs/heads/`), the HEAD is detached.\n     +\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n     +\n      `-u` _<upload-pack>_::\n    @@ builtin/clone.c: static struct option builtin_clone_options[] = {\n      \t\t   N_(\"path to git-upload-pack on the remote\")),\n      \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n     @@ builtin/clone.c: static void update_head(const struct ref *our, const struct ref *remote,\n    - \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n    - \t\t}\n      \t} else if (our) {\n    --\t\tstruct commit *c = lookup_commit_reference(the_repository,\n    --\t\t\t\t\t\t\t   &our->old_oid);\n    --\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n    --\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n    --\t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n    --\t\t\t\tUPDATE_REFS_DIE_ON_ERR);\n    -+\t\tstruct commit *c =\n    -+\t\t\tlookup_commit_reference(the_repository, &our->old_oid);\n    + \t\tstruct commit *c = lookup_commit_reference(the_repository,\n    + \t\t\t\t\t\t\t   &our->old_oid);\n    ++\t\tif (!c)\n    ++\t\t\tdie(_(\"unable to update HEAD\"));\n     +\n    -+\t\tif (c)\n    -+\t\t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n    -+\t\t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n    -+\t\t\t\t\t\"HEAD\", &c->object.oid, NULL,\n    -+\t\t\t\t\tREF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR);\n    - \t} else if (remote) {\n    - \t\t/*\n    - \t\t * We know remote HEAD points to a non-branch, or\n    + \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n    + \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n    + \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n     @@ builtin/clone.c: static void write_refspec_config(const char *src_ref_prefix,\n      \tstruct strbuf key = STRBUF_INIT;\n      \tstruct strbuf value = STRBUF_INIT;\n    @@ builtin/clone.c: int cmd_clone(int argc,\n      \n      \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n      \n    -+\tif (option_rev) {\n    -+\t\tif (option_branch)\n    -+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n    -+\t\t\t    \"--revision\", \"--branch\");\n    -+\t\tif (option_mirror)\n    -+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n    -+\t\t\t    \"--revision\", \"--mirror\");\n    -+\t\tif (option_bare)\n    -+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n    -+\t\t\t    \"--revision\", \"--bare\");\n    -+\t}\n    ++\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n    ++\t\t\t\t  !!option_branch, \"--branch\");\n    ++\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n    ++\t\t\t\t  option_mirror, \"--mirror\");\n     +\n      \tif (reject_shallow)\n      \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n    @@ builtin/clone.c: int cmd_clone(int argc,\n      \t\t\t    option_branch, remote_name);\n     +\t} else if (option_rev) {\n     +\t\tour_head_points_at = mapped_refs;\n    ++\t\tif (!our_head_points_at)\n    ++\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n    ++\t\t\t    option_rev, remote_name);\n      \t} else if (remote_head_points_at) {\n      \t\tour_head_points_at = remote_head_points_at;\n      \t} else if (remote_head) {\n     \n    + ## parse-options.h ##\n    +@@ parse-options.h: static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n    + \t\t\t\t  0, \"\");\n    + }\n    + \n    ++static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n    ++\t\t\t\t\t     int opt2, const char *opt2_name)\n    ++{\n    ++\tdie_for_incompatible_opt4(opt1, opt1_name,\n    ++\t\t\t\t  opt2, opt2_name,\n    ++\t\t\t\t  0, \"\",\n    ++\t\t\t\t  0, \"\");\n    ++}\n    ++\n    + /*\n    +  * Use these assertions for callbacks that expect to be called with NONEG and\n    +  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n    +\n    + ## t/meson.build ##\n    +@@ t/meson.build: integration_tests = [\n    +   't5617-clone-submodules-remote.sh',\n    +   't5618-alternate-refs.sh',\n    +   't5619-clone-local-ambiguous-transport.sh',\n    ++  't5620-clone-revision.sh',\n    +   't5700-protocol-v1.sh',\n    +   't5701-git-serve.sh',\n    +   't5702-protocol-v2.sh',\n    +\n      ## t/t5620-clone-revision.sh (new) ##\n     @@\n     +#!/bin/sh\n    @@ t/t5620-clone-revision.sh (new)\n     +\n     +test_expect_success 'setup' '\n     +\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n    -+\ttest_commit \"second commit\" README \"Hello world\" v1.0 &&\n    ++\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n     +\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n     +\tgit switch -c feature v1.0 &&\n     +\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n    @@ t/t5620-clone-revision.sh (new)\n     +test_expect_success 'clone with --revision being a tag' '\n     +\ttest_when_finished \"rm -rf dst\" &&\n     +\tgit clone --revision=refs/tags/v1.0 . dst &&\n    -+\tgit rev-parse refs/tags/v1.0 >expect &&\n    ++\tgit rev-parse refs/tags/v1.0^{} >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n     +\ttest_cmp expect actual\n     +'\n    @@ t/t5620-clone-revision.sh (new)\n     +\ttest_cmp expect actual\n     +'\n     +\n    ++test_expect_success 'clone with --revision and --bare' '\n    ++\ttest_when_finished \"rm -rf dst\" &&\n    ++\tgit clone --revision=refs/heads/main --bare . dst &&\n    ++\toid=$(git rev-parse refs/heads/main) &&\n    ++\tgit -C dst cat-file -t $oid > actual &&\n    ++\techo \"commit\" >expect &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n     +test_expect_success 'clone with --revision being a short raw commit hash' '\n     +\ttest_when_finished \"rm -rf dst\" &&\n     +\toid=$(git rev-parse --short refs/heads/feature) &&\n    -+\tgit clone --revision=$oid . dst 2>err &&\n    -+\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n    ++\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n    ++\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n     +'\n     +\n     +test_expect_success 'clone with --revision being a tree hash' '\n     +\ttest_when_finished \"rm -rf dst\" &&\n     +\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n    -+\tgit clone --revision=$oid . dst 2>err &&\n    -+\tgrep \"warning: remote HEAD refers to nonexistent ref, unable to checkout\" err\n    ++\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n    ++\ttest_grep \"error: object $oid is a tree, not a commit\" err\n     +'\n     +\n     +test_expect_success 'clone with --revision being the parent of a ref fails' '\n    @@ t/t5620-clone-revision.sh (new)\n     +\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n     +'\n     +\n    -+test_expect_success 'clone with --revision and --bare fails' '\n    -+\ttest_when_finished \"rm -rf dst\" &&\n    -+\ttest_must_fail git clone --revision=refs/heads/main --bare . dst\n    -+'\n    -+\n     +test_done\n---\n Documentation/git-clone.txt |   8 ++++\n builtin/clone.c             |  49 +++++++++++++++++++--\n parse-options.h             |   9 ++++\n t/meson.build               |   1 +\n t/t5620-clone-revision.sh   | 101 ++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 164 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex de8d8f58930ecff305f79480b13ddce10cd96c60..67498dae7c7d0315c7026b4ca2e822e48dcb7479 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -221,6 +221,14 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision` _<rev>_::\n+\tThis clones the given revision, and that revision only. The argument can\n+\tbe a ref name (e.g. `refs/heads/main`) that peels down to a commit, or a\n+\traw commit hash.\n+\tThe given revision is checked out, and for any revision other than a\n+\tbranch (i.e. ref starting with `refs/heads/`), the HEAD is detached.\n+\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 21721db28a12864621d27d2c70ee5c2598cccc0f..391757e5260a902a87bcf3b435fe39c6cd841b3b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -69,6 +69,7 @@ static char *option_template, *option_depth, *option_since;\n static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n+static char *option_rev = NULL;\n static struct string_list option_not = STRING_LIST_INIT_NODUP;\n static const char *real_git_dir;\n static const char *ref_format;\n@@ -141,6 +142,8 @@ static struct option builtin_clone_options[] = {\n \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t   N_(\"clone single revision <rev> and check out\")),\n \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t   N_(\"path to git-upload-pack on the remote\")),\n \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -684,6 +687,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t} else if (our) {\n \t\tstruct commit *c = lookup_commit_reference(the_repository,\n \t\t\t\t\t\t\t   &our->old_oid);\n+\t\tif (!c)\n+\t\t\tdie(_(\"unable to update HEAD\"));\n+\n \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n@@ -889,7 +895,7 @@ static void write_refspec_config(const char *src_ref_prefix,\n \tstruct strbuf key = STRBUF_INIT;\n \tstruct strbuf value = STRBUF_INIT;\n \n-\tif (option_mirror || !option_bare) {\n+\tif (!option_rev && (option_mirror || !option_bare)) {\n \t\tif (option_single_branch && !option_mirror) {\n \t\t\tif (option_branch) {\n \t\t\t\tif (starts_with(our_head_points_at->name, \"refs/tags/\"))\n@@ -968,7 +974,7 @@ int cmd_clone(int argc,\n \tchar *repo_to_free = NULL;\n \tchar *path = NULL, *dir, *display_repo = NULL;\n \tint dest_exists, real_dest_exists = 0;\n-\tconst struct ref *refs, *remote_head;\n+\tconst struct ref *refs = NULL, *remote_head;\n \tstruct ref *remote_head_points_at = NULL;\n \tconst struct ref *our_head_points_at;\n \tchar *unborn_head = NULL;\n@@ -1345,6 +1351,11 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  !!option_branch, \"--branch\");\n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  option_mirror, \"--mirror\");\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1387,7 +1398,27 @@ int cmd_clone(int argc,\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n-\trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n+\tif (option_rev) {\n+\t\trefspec_clear(&remote->fetch);\n+\t\trefspec_init(&remote->fetch, REFSPEC_FETCH);\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\n+\t\tstrvec_clear(&transport_ls_refs_options.ref_prefixes);\n+\t\trefspec_ref_prefixes(&remote->fetch,\n+\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n+\n+\t\tif (transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\t\t/*\n+\t\t\t * We need to talk to the server to determine the hash\n+\t\t\t * algorithm, but when no ref prefixes are set the\n+\t\t\t * server announces all known refs, so ask the server to\n+\t\t\t * only tell us about HEAD.\n+\t\t\t */\n+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\t}\n+\n+\trefs = transport_get_remote_refs(transport,\n+\t\t\t\t\t &transport_ls_refs_options);\n \n \t/*\n \t * Now that we know what algorithm the remote side is using, let's set\n@@ -1461,8 +1492,13 @@ int cmd_clone(int argc,\n \t\t}\n \t}\n \n-\tif (refs)\n+\tif (option_rev) {\n+\t\tstruct ref **tail = &mapped_refs;\n+\n+\t\tget_fetch_map(refs, &remote->fetch.items[0], &tail, 1);\n+\t} else if (refs) {\n \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t}\n \n \tif (mapped_refs) {\n \t\t/*\n@@ -1495,6 +1531,11 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n+\t\tif (!our_head_points_at)\n+\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n+\t\t\t    option_rev, remote_name);\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\ndiff --git a/parse-options.h b/parse-options.h\nindex f0801d4532a175b65783689f2a68fb56da2c8e87..72c62311b61f46152d66bcba9328de59fd300df9 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -420,6 +420,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n \t\t\t\t  0, \"\");\n }\n \n+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n+\t\t\t\t\t     int opt2, const char *opt2_name)\n+{\n+\tdie_for_incompatible_opt4(opt1, opt1_name,\n+\t\t\t\t  opt2, opt2_name,\n+\t\t\t\t  0, \"\",\n+\t\t\t\t  0, \"\");\n+}\n+\n /*\n  * Use these assertions for callbacks that expect to be called with NONEG and\n  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\ndiff --git a/t/meson.build b/t/meson.build\nindex 13fe854ba0a18f9b83dbc48651f581198042ffd3..606fd6ff9d09f5c90c08ee4b9e108a5a04e187d9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -721,6 +721,7 @@ integration_tests = [\n   't5617-clone-submodules-remote.sh',\n   't5618-alternate-refs.sh',\n   't5619-clone-local-ambiguous-transport.sh',\n+  't5620-clone-revision.sh',\n   't5700-protocol-v1.sh',\n   't5701-git-serve.sh',\n   't5702-protocol-v2.sh',\ndiff --git a/t/t5620-clone-revision.sh b/t/t5620-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..92fe0b3f1c1268fc29e0e61d543167ea35845066\n--- /dev/null\n+++ b/t/t5620-clone-revision.sh\n@@ -0,0 +1,101 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\tgit for-each-ref refs/heads/feature > expect &&\n+\tgit -C dst for-each-ref > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0^{} >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision and --bare' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/main --bare . dst &&\n+\toid=$(git rev-parse refs/heads/main) &&\n+\tgit -C dst cat-file -t $oid > actual &&\n+\techo \"commit\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"error: object $oid is a tree, not a commit\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_done\n\n---\n\nbase-commit: d882f382b3d939d90cfa58d17b17802338f05d66\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nThanks\n--\nToon\n\n"},{"id":"509342","messageId":"xmqqh66zikys.fsf@gitster.g","threadId":"62566","inReplyTo":"20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com","subject":"Re: [PATCH v3] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-12-19T18:30:51Z","receivedAt":"2024-12-19T18:30:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> The git-clone(1) command has the option `--branch` that allows the user\n> to select the branch they want HEAD to point to. In a non-bare\n> repository this also checks out that branch.\n>\n> Option `--branch` also accepts a tag. When a tag name is provided, the\n> commit this tag points to is checked out and HEAD is detached. Thus\n> `--branch` can be used to clone a repository and check out a ref kept\n> under `refs/heads` or `refs/tags`. But some other refs might be in use\n> as well. For example Git forges might use refs like `refs/pull/<id>` and\n> `refs/merge-requests/<id>` to track pull/merge requests. These refs\n> cannot be selected upon git-clone(1).\n>\n> Add option `--revision` to git-clone(1). This option accepts a fully\n> qualified reference, or a raw commit hash. This enables the user to\n> clone and check out any revision they want. `--revision` can be used in\n> conjunction with `--depth` to do a minimal clone that only contains the\n> sources for a single revision. This can be useful for automated tests\n> running in CI systems.\n\nI somehow suspect that you want the mental model your users form\nfor this new feature to be \"--single-branch --branch=<branch>\" mode\nof \"git clone\", not just \"--branch=<branch>\".  This is especially true\nwhen you bring up the possibility of the \"--depth\" option.\n\nThe difference between \"git clone\" and \"git init\" followed by \"git\nfetch\" is not just \"the user does not have to type two commands if\nthe feature is inside clone\".  That is merely a superficial\ndifference, and the real value is that \"clone\" leaves configuration\nto make further work easier.\n\nFor the \"--branch\" option the user's mental model for this new\nfeature is based on, \"git clone\" gives the usual fetch pathspec\nand the branch.merge configuration to make it build on top of the\nsingle remote branch we cloned, i.e. you'd get\n\n\t[remote \"origin\"]\n\t\turl = $URL\n\t\tfetch = +refs/heads/*:refs/remotes/origin/*\n\t[branch \"next\"]\n\t\tremote = origin\n\t\tmerge = refs/heads/next\n\nwith \"git clone --branch=next $URL\".  \n\nBut after \"git clone --single-branch --branch=next $URL\", you'd get\n\n\t[remote \"origin\"]\n\t\turl = $URL\n\t\tfetch = +refs/heads/next:refs/remotes/origin/next\n\t[branch \"next\"]\n\t\tremote = origin\n\t\tmerge = refs/heads/next\n\nAfter all, the user wants to work with this single branch, so we\nonly dedicate a single remote-tracking branch for that and ignore\neverything else.\n\nWith that line of thought in mind, let's read on with this question\nin mind: what configuration should this mode of \"git clone\" leave,\nto make further work in the resulting repository easier?\n\n> This type of shallow clone could also be achieved with the following set\n> of commands:\n>\n>     git init the-repo\n>     cd ./the-repo\n\nOK.\n\n>     git remote add origin <url>\n\nNot a great idea, as it is likely to leave remote.origin.fetch\nrefspec that is overly wide.  Compared to \"--single-branch --branch\"\ncase, I suspect that the resulting configuration should just record\nremote.origin.url but not remote.origin.fetch or branch.*.* at all,\nor something like that, to make sure we won't track any refs from\nthere.\n\n>     git fetch --depth=1 origin <commit-id>\n>     git checkout <commit-id>\n\nOK, but \"git checkout --detach <commit>\" would make it clear that we\nare not getting any local branch.\n\n> Adding this new option to git-clone(1) simplifies this not uncommon\n> use-case. And besides simplifying this, it enables the use of\n> git-clone(1) over git-fetch(1). This is beneficial in case bundle URIs\n> are available on the server. Bundle URIs are only used on clone, not on\n> fetch, so using allowing the user to use git-clone(1) here makes them\n> benefit from bundle URIs if advertised by the server.\n\nThat reasoning goes backwards.  If this wants to be really a\none-time \"git fetch\", but the lack of features like bundleURI\nsupport in it makes your choice of tool unworkable, we should be\nadding the support to your chosen tool, \"git fetch\".\n\nAs I already said, we should sell \"clone\" not for the stupid \"the\nuser do not have to do init and fetch separately\" reason, but \"we\ncan tell what workflow is expected from the command line options\ngiven to 'git clone', and configure the resulting repository\nappropriately, which is not something init+fetch cannot do\".\n\nAnd when you sell it that way, bundleURI becomes totally a\nnon-issue (it becomes a mere bonus cherry-on-top).\n\n> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> index de8d8f58930ecff305f79480b13ddce10cd96c60..67498dae7c7d0315c7026b4ca2e822e48dcb7479 100644\n> --- a/Documentation/git-clone.txt\n> +++ b/Documentation/git-clone.txt\n> @@ -221,6 +221,14 @@ objects from the source repository into a pack in the cloned repository.\n>  \t`--branch` can also take tags and detaches the `HEAD` at that commit\n>  \tin the resulting repository.\n>  \n> +`--revision` _<rev>_::\n> +\tThis clones the given revision, and that revision only.\n\n\"that revision only\" sounds like this option automatically forces \"--depth=1\",\nbut I do not think that is what we want.\n\n\tCreate a new repository, and fetch the history leading to\n\tthe given revision _<rev>_ (and nothing else), without\n\tmaking any remote-tracking branch, and without making any\n\tlocal branch, and point `HEAD` to _<rev_>.  When creating a\n\tnon-bare repository, the revision is checked out on a\n\tdetached `HEAD`.\n\nor something?\n\n> +\tThe argument can\n> +\tbe a ref name (e.g. `refs/heads/main`) that peels down to a commit, or a\n> +\traw commit hash.\n\n\"raw\" -> \"hexadecimal\", \"hash\" -> \"object name\", but otherwise looks\ngood.  A ref-name may have an example for tag as well, i.e.\n\n\t(e.g., `refs/heads/main` or `refs/tags/v1.0`)\n\n> +\tThe given revision is checked out, and for any revision other than a\n> +\tbranch (i.e. ref starting with `refs/heads/`), the HEAD is detached.\n\n\"--revision refs/heads/master~4\" start with \"refs/heads/\" but I do\nnot think you want to special case it.  You'd want to always detach\nfor consistency and simplicity.  If they want branch, the will say\n\"--single-branch --branch\" instead of \"--revision\".\n\n> +\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n\nHow does this intarct with \"--single-branch\"?  It is obvious and\nnatural to ignore \"--single-branch\" as you are not going to create\nany local or remote-tracking branches, but should this be made\nincompatible with \"--no-single-branch\"?  I dunno.\n\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 21721db28a12864621d27d2c70ee5c2598cccc0f..391757e5260a902a87bcf3b435fe39c6cd841b3b 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -69,6 +69,7 @@ static char *option_template, *option_depth, *option_since;\n>  static char *option_origin = NULL;\n>  static char *remote_name = NULL;\n>  static char *option_branch = NULL;\n> +static char *option_rev = NULL;\n>  static struct string_list option_not = STRING_LIST_INIT_NODUP;\n>  static const char *real_git_dir;\n>  static const char *ref_format;\n> @@ -141,6 +142,8 @@ static struct option builtin_clone_options[] = {\n>  \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n>  \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n>  \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n> +\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n> +\t\t   N_(\"clone single revision <rev> and check out\")),\n>  \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n>  \t\t   N_(\"path to git-upload-pack on the remote\")),\n>  \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n> @@ -684,6 +687,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>  \t} else if (our) {\n>  \t\tstruct commit *c = lookup_commit_reference(the_repository,\n>  \t\t\t\t\t\t\t   &our->old_oid);\n> +\t\tif (!c)\n> +\t\t\tdie(_(\"unable to update HEAD\"));\n\nThis new error condition becomes necessary, because you have no way\nto verify the value given to --revision until you see it here, which\nis understandable.\n\nWhat are the failure modes and causes?  Can we give a bit more\nuseful diagnostics, like\n\n - the named object did not exist on the remote repository at all\n - failed to fetch the named object from the remote repository\n - fetched the named object and it turns out not to be a committish\n - something else?\n\n> @@ -968,7 +974,7 @@ int cmd_clone(int argc,\n>  \tchar *repo_to_free = NULL;\n>  \tchar *path = NULL, *dir, *display_repo = NULL;\n>  \tint dest_exists, real_dest_exists = 0;\n> -\tconst struct ref *refs, *remote_head;\n> +\tconst struct ref *refs = NULL, *remote_head;\n>  \tstruct ref *remote_head_points_at = NULL;\n>  \tconst struct ref *our_head_points_at;\n>  \tchar *unborn_head = NULL;\n\nOffhand I do not see why this hunk is needed.\n\n> diff --git a/parse-options.h b/parse-options.h\n> index f0801d4532a175b65783689f2a68fb56da2c8e87..72c62311b61f46152d66bcba9328de59fd300df9 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -420,6 +420,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n>  \t\t\t\t  0, \"\");\n>  }\n>  \n> +static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n> +\t\t\t\t\t     int opt2, const char *opt2_name)\n> +{\n> +\tdie_for_incompatible_opt4(opt1, opt1_name,\n> +\t\t\t\t  opt2, opt2_name,\n> +\t\t\t\t  0, \"\",\n> +\t\t\t\t  0, \"\");\n> +}\n> +\n\nFunny that we had 4 and 3 but did not need 2 until now ;-)\n\n> diff --git a/t/t5620-clone-revision.sh b/t/t5620-clone-revision.sh\n\nThere already is a topic in flight that uses 5620 for its new test.\n\n> @@ -0,0 +1,101 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git clone --revision'\n> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n\n> +TEST_PASSES_SANITIZE_LEAK=true\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' '\n> +\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n> +\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n> +\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n> +\tgit switch -c feature v1.0 &&\n> +\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n> +\tgit switch main\n> +'\n\n> +test_expect_success 'clone with --revision being a branch' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=refs/heads/feature . dst &&\n> +\tgit rev-parse refs/heads/feature >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual &&\n\nThis validates that the checked out commit is the same named commit,\nbut does not see whether we are on a detached HEAD or not.  Even\nafter we change the implementation to always detach, up to this\npoint the test will pass (I am not saying it is great.  I am saying\nthat the test is too loose to etch the desired behaviour in stone).\n\nThe next line, however, expects that we create a local branch for it\n(the design I do not necessarily agree with---if this is wanted, the\nuser can already use --branch with --single-branch to do so).\n\n> +\tgit for-each-ref refs/heads/feature > expect &&\n> +\tgit -C dst for-each-ref > actual &&\n\nStyle: no spaces between redirection operator and its target file.\n\n> +\ttest_cmp expect actual\n> +'\n\nWe should check the configuration in the resulting repository.  If\nwe do not care what configuration \"clone\" leaves, we are not getting\nthe benefit of using it (you can just do init + fetch + checkout\ninstead).  This applies to all the remaining tests, so I won't repeat.\n\n> +test_expect_success 'clone with --depth and --revision being a branch' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n> +\tgit rev-parse refs/heads/feature >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nWe are not validating if the depth did its task correctly here.  We\ndo not test if the resulting repository has it checked out on the\ndetached HEAD.  We do not test if the resulting repository has the\nlocal branch of the same name.\n\n> +test_expect_success 'clone with --revision being a tag' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=refs/tags/v1.0 . dst &&\n> +\tgit rev-parse refs/tags/v1.0^{} >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nIt is a good idea to verify an annotated tag, as you never want to\nwrite anything but a commit in HEAD.\n\nAgain, the tests are too loose to validate anything important.\n\n> +test_expect_success 'clone with --revision being HEAD' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\tgit clone --revision=HEAD . dst &&\n> +\tgit rev-parse HEAD >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nDitto.  I presume that this wants to detach and without any local or\nremote-tracking branches left?\n\n> +test_expect_success 'clone with --revision being a raw commit hash' '\n> +\ttest_when_finished \"rm -rf dst\" &&\n> +\toid=$(git rev-parse refs/heads/feature) &&\n> +\tgit clone --revision=$oid . dst &&\n> +\techo $oid >expect &&\n> +\tgit -C dst rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nDitto.  It is common across many of the tests that they are too\nloose and do not check much beyond the value of HEAD, which wants\ncorrected.  My recommendation for the behaviour is for all cases\nthey do not touch either local or remote-tracking branches and\nalways detach HEAD at the commit (or fail if a non-committish was\ngiven), which means the all the tests (other than the ones that\nmakes sure the command fails under some conditions) should check\nthat the HEAD is detached at the expected commit and no local or\nremote-tracking branches created.  This is shared among the rest of\nthe test, so I won't repeat.\n\nThanks.\n"},{"id":"511567","messageId":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com","subject":"[PATCH v4 0/6] Enable doing a shallow clone of a specific git revision","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:28Z","receivedAt":"2025-01-31T15:30:59Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The goal of this series is to add an option `--revision` to\ngit-clone(1).\n\nCompared to previous version, I'm adding a handful of prepatory\nrefactoring commits that make it more straight-forward to add this new\noption.\n\nThis series sets an example on how I think we can further refactor\nbuiltin/clone.c to increase the maintainability of the code.\n\n---\nChanges in v4:\n- Introduce a new commit to reduce the use of global variables.\n- Introduce a new commit to invert the flag --no-tags to --tags.\n- Introduce a new commit to refactor wanted_peer_refs() in\n  builtin/clone.c.\n- Introduce a new commit to shuffle the handling of tags refspec.\n- Introduce a new commit to introduce a `struct clone_opts`.\n- Link to v3: https://lore.kernel.org/r/20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com\n\nChanges in v3:\n- Fail early when the revision was not found on the remote, instead of\n  creating a clone that's in an invalid state.\n- State more clearly in the commit message adding this option is useful\n  for a not uncommon use-case.\n- Be explicit in the documentation the ref needs to peel down to a\n  commit.\n- Die in case we try to update_head() to an object that's not a commit.\n- Allow combining `--revision` with `--bare`.\n- Add die_for_incompatible_opt2() to parse-options.h and use it for the\n  options that are not compatible with the new `--revision` option.\n- Small tweaks to the added tests.\n- Small touchups on commit messages.\n- Link to v2: https://lore.kernel.org/r/20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com\n\n---\nToon Claes (6):\n      clone: cut down on global variables in clone.c\n      clone: make it possible to specify --tags\n      clone: refactor wanted_peer_refs()\n      clone: add tags refspec earlier to fetch refspec\n      clone: introduce struct clone_opts in builtin/clone.c\n      builtin/clone: teach git-clone(1) the --revision= option\n\n Documentation/git-clone.txt |  17 ++-\n builtin/clone.c             | 345 +++++++++++++++++++++++++-------------------\n parse-options.h             |   9 ++\n remote.c                    |   2 +-\n remote.h                    |   1 +\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++\n 7 files changed, 345 insertions(+), 153 deletions(-)\n---\n\nRange-diff versus v3:\n\n-:  ---------- > 1:  b16d266d95 clone: cut down on global variables in clone.c\n-:  ---------- > 2:  8a78ad94ac clone: make it possible to specify --tags\n-:  ---------- > 3:  156602ecda clone: refactor wanted_peer_refs()\n-:  ---------- > 4:  1df83a1207 clone: add tags refspec earlier to fetch refspec\n-:  ---------- > 5:  8acaad897d clone: introduce struct clone_opts in builtin/clone.c\n1:  6424e444c1 ! 6:  ed3195d705 builtin/clone: teach git-clone(1) the --revision= option\n    @@ Commit message\n         cannot be selected upon git-clone(1).\n     \n         Add option `--revision` to git-clone(1). This option accepts a fully\n    -    qualified reference, or a raw commit hash. This enables the user to\n    -    clone and check out any revision they want. `--revision` can be used in\n    -    conjunction with `--depth` to do a minimal clone that only contains the\n    -    sources for a single revision. This can be useful for automated tests\n    -    running in CI systems.\n    +    qualified reference, or a hexadecimal commit ID. This enables the user\n    +    to clone and check out any revision they want. `--revision` can be used\n    +    in conjunction with `--depth` to do a minimal clone that only contains\n    +    the blob and tree for a single revision. This can be useful for\n    +    automated tests running in CI systems.\n     \n    -    This type of shallow clone could also be achieved with the following set\n    -    of commands:\n    +    Using option `--branch` and `--single-branch` together is a similar\n    +    scenario, but serves a different purpose. Using these two options, a\n    +    singlet remote tracking branch is created and the fetch refspec is set\n    +    up so git-fetch(1) will receive updates on that branch from the remote.\n    +    This allows the user work on that single branch.\n     \n    -        git init the-repo\n    -        cd ./the-repo\n    -        git remote add origin <url>\n    -        git fetch --depth=1 origin <commit-id>\n    -        git checkout <commit-id>\n    -\n    -    Adding this new option to git-clone(1) simplifies this not uncommon\n    -    use-case. And besides simplifying this, it enables the use of\n    -    git-clone(1) over git-fetch(1). This is beneficial in case bundle URIs\n    -    are available on the server. Bundle URIs are only used on clone, not on\n    -    fetch, so using allowing the user to use git-clone(1) here makes them\n    -    benefit from bundle URIs if advertised by the server.\n    +    Option `--revision` on contrary detaches HEAD, creates no tracking\n    +    branches, and writes no fetch refspec.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    @@ Documentation/git-clone.txt: objects from the source repository into a pack in t\n      \tin the resulting repository.\n      \n     +`--revision` _<rev>_::\n    -+\tThis clones the given revision, and that revision only. The argument can\n    -+\tbe a ref name (e.g. `refs/heads/main`) that peels down to a commit, or a\n    -+\traw commit hash.\n    -+\tThe given revision is checked out, and for any revision other than a\n    -+\tbranch (i.e. ref starting with `refs/heads/`), the HEAD is detached.\n    -+\tThis option is incompatible with `--branch`, `--mirror`, and `--bare`.\n    ++\tCreate a new repository, and fetch the history leading to the given\n    ++\trevision _<rev>_ (and nothing else), without making any remote-tracking\n    ++\tbranch, and without making any local branch, and point `HEAD` to\n    ++\t_<rev>_. When creating a non-bare repository, the revision is checked\n    ++\tout on a detached `HEAD`. The argument can be a ref name\n    ++\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n    ++\tcommit, or a hexadecimal object name.\n    ++\tThis option is incompatible with `--branch` and `--mirror`.\n     +\n      `-u` _<upload-pack>_::\n      `--upload-pack` _<upload-pack>_::\n      \tWhen given, and the repository to clone from is accessed\n     \n      ## builtin/clone.c ##\n    -@@ builtin/clone.c: static char *option_template, *option_depth, *option_since;\n    - static char *option_origin = NULL;\n    - static char *remote_name = NULL;\n    - static char *option_branch = NULL;\n    -+static char *option_rev = NULL;\n    - static struct string_list option_not = STRING_LIST_INIT_NODUP;\n    - static const char *real_git_dir;\n    - static const char *ref_format;\n    -@@ builtin/clone.c: static struct option builtin_clone_options[] = {\n    - \t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n    - \tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n    - \t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n    -+\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n    -+\t\t   N_(\"clone single revision <rev> and check out\")),\n    - \tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n    - \t\t   N_(\"path to git-upload-pack on the remote\")),\n    - \tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n    +@@\n    + \n    + struct clone_opts {\n    + \tint wants_head;\n    ++\tint detach;\n    + };\n    + #define CLONE_OPTS_INIT { \\\n    + \t.wants_head = 1 /* default enabled */ \\\n    +@@ builtin/clone.c: static void update_remote_refs(const struct ref *refs,\n    + \t}\n    + }\n    + \n    +-static void update_head(const struct ref *our, const struct ref *remote,\n    ++static void update_head(struct clone_opts *opts, const struct ref *our, const struct ref *remote,\n    + \t\t\tconst char *unborn, const char *msg)\n    + {\n    + \tconst char *head;\n    +-\tif (our && skip_prefix(our->name, \"refs/heads/\", &head)) {\n    ++\tif (our && !opts->detach && skip_prefix(our->name, \"refs/heads/\", &head)) {\n    + \t\t/* Local default branch link */\n    + \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n    + \t\t\tdie(_(\"unable to update HEAD\"));\n     @@ builtin/clone.c: static void update_head(const struct ref *our, const struct ref *remote,\n      \t} else if (our) {\n      \t\tstruct commit *c = lookup_commit_reference(the_repository,\n      \t\t\t\t\t\t\t   &our->old_oid);\n    ++\n     +\t\tif (!c)\n    -+\t\t\tdie(_(\"unable to update HEAD\"));\n    ++\t\t\tdie(_(\"couldn't look up commit object for '%s'\"), our->name);\n     +\n      \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n      \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n      \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n    -@@ builtin/clone.c: static void write_refspec_config(const char *src_ref_prefix,\n    - \tstruct strbuf key = STRBUF_INIT;\n    - \tstruct strbuf value = STRBUF_INIT;\n    +@@ builtin/clone.c: int cmd_clone(int argc,\n    + \tint option_filter_submodules = -1; /* unspecified */\n    + \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n    + \tconst char *bundle_uri = NULL;\n    ++\tchar *option_rev = NULL;\n    + \n    + \tstruct clone_opts opts = CLONE_OPTS_INIT;\n    + \n    +@@ builtin/clone.c: int cmd_clone(int argc,\n    + \t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n    + \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n    + \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n    ++\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n    ++\t\t\t   N_(\"clone single revision <rev> and check out\")),\n    + \t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n    + \t\t\t   N_(\"path to git-upload-pack on the remote\")),\n    + \t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n    +@@ builtin/clone.c: int cmd_clone(int argc,\n    + \t\tstrbuf_addstr(&branch_top, src_ref_prefix);\n    + \n    + \t\tgit_config_set(\"core.bare\", \"true\");\n    +-\t} else {\n    ++\t} else if (!option_rev) {\n    + \t\tstrbuf_addf(&branch_top, \"refs/remotes/%s/\", remote_name);\n    + \t}\n      \n    --\tif (option_mirror || !option_bare) {\n    -+\tif (!option_rev && (option_mirror || !option_bare)) {\n    - \t\tif (option_single_branch && !option_mirror) {\n    - \t\t\tif (option_branch) {\n    - \t\t\t\tif (starts_with(our_head_points_at->name, \"refs/tags/\"))\n     @@ builtin/clone.c: int cmd_clone(int argc,\n    - \tchar *repo_to_free = NULL;\n    - \tchar *path = NULL, *dir, *display_repo = NULL;\n    - \tint dest_exists, real_dest_exists = 0;\n    --\tconst struct ref *refs, *remote_head;\n    -+\tconst struct ref *refs = NULL, *remote_head;\n    - \tstruct ref *remote_head_points_at = NULL;\n    - \tconst struct ref *our_head_points_at;\n    - \tchar *unborn_head = NULL;\n    + \n    + \tremote = remote_get_early(remote_name);\n    + \n    +-\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n    +-\t\t\tbranch_top.buf);\n    ++\tif (!option_rev)\n    ++\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n    ++\t\t\t\tbranch_top.buf);\n    + \n    + \tpath = get_repo_path(remote->url.v[0], &is_bundle);\n    + \tis_local = option_local != 0 && path && !is_bundle;\n     @@ builtin/clone.c: int cmd_clone(int argc,\n      \n      \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\t\t\t\t  !!option_branch, \"--branch\");\n     +\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n     +\t\t\t\t  option_mirror, \"--mirror\");\n    ++\t// TODO --no-single-branch\n     +\n      \tif (reject_shallow)\n      \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n      \tif (option_depth)\n     @@ builtin/clone.c: int cmd_clone(int argc,\n    - \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n    - \t\t\t    \"refs/tags/\");\n    + \tif (transport->smart_options && !deepen && !filter_options.choice)\n    + \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n      \n    --\trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n    +-\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n     +\tif (option_rev) {\n    -+\t\trefspec_clear(&remote->fetch);\n    -+\t\trefspec_init(&remote->fetch, REFSPEC_FETCH);\n    -+\t\trefspec_append(&remote->fetch, option_rev);\n    ++\t\toption_tags = 0;\n    ++\t\toption_branch = 0;\n    ++\t\toption_single_branch = 0;\n    ++\t\topts.wants_head = 0;\n    ++\t\topts.detach = 1;\n     +\n    -+\t\tstrvec_clear(&transport_ls_refs_options.ref_prefixes);\n    -+\t\trefspec_ref_prefixes(&remote->fetch,\n    -+\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n    -+\n    -+\t\tif (transport_ls_refs_options.ref_prefixes.nr == 0)\n    -+\t\t\t/*\n    -+\t\t\t * We need to talk to the server to determine the hash\n    -+\t\t\t * algorithm, but when no ref prefixes are set the\n    -+\t\t\t * server announces all known refs, so ask the server to\n    -+\t\t\t * only tell us about HEAD.\n    -+\t\t\t */\n    -+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n    ++\t\trefspec_append(&remote->fetch, option_rev);\n     +\t}\n    -+\n    -+\trefs = transport_get_remote_refs(transport,\n    -+\t\t\t\t\t &transport_ls_refs_options);\n      \n    - \t/*\n    - \t * Now that we know what algorithm the remote side is using, let's set\n    + \tif (option_tags || option_branch)\n    + \t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n     @@ builtin/clone.c: int cmd_clone(int argc,\n    - \t\t}\n    - \t}\n    + \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n    + \t\t\t\t  option_branch);\n      \n    --\tif (refs)\n    -+\tif (option_rev) {\n    -+\t\tstruct ref **tail = &mapped_refs;\n    ++\t/*\n    ++\t * As part of transport_get_remote_refs() the server tells us the hash\n    ++\t * algorithm, which we require to initialize the repo. But calling that\n    ++\t * function without any ref prefix, will cause the server to announce\n    ++\t * all known refs. If the argument passed to --revision was a hex oid,\n    ++\t * ref_prefixes will be empty so we fall back to asking about HEAD to\n    ++\t * reduce traffic from the server.\n    ++\t */\n    ++\tif (opts.wants_head || transport_ls_refs_options.ref_prefixes.nr == 0)\n    ++\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n     +\n    -+\t\tget_fetch_map(refs, &remote->fetch.items[0], &tail, 1);\n    -+\t} else if (refs) {\n    - \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n    -+\t}\n    + \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n      \n    - \tif (mapped_refs) {\n    - \t\t/*\n    + \t/*\n     @@ builtin/clone.c: int cmd_clone(int argc,\n      \t\tif (!our_head_points_at)\n      \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\t\tif (!our_head_points_at)\n     +\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n     +\t\t\t    option_rev, remote_name);\n    ++\t\t//mapped_refs->name[0] = 0;\n      \t} else if (remote_head_points_at) {\n      \t\tour_head_points_at = remote_head_points_at;\n      \t} else if (remote_head) {\n    +@@ builtin/clone.c: int cmd_clone(int argc,\n    + \t\tfree(to_free);\n    + \t}\n    + \n    +-\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n    +-\t\t\tremote_head_points_at, &branch_top);\n    ++\tif (!option_rev)\n    ++\t\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n    ++\t\t\t\t     remote_head_points_at, &branch_top);\n    + \n    + \tif (filter_options.choice)\n    + \t\tpartial_clone_register(remote_name, &filter_options);\n    +@@ builtin/clone.c: int cmd_clone(int argc,\n    + \t\t\t   branch_top.buf, reflog_msg.buf, transport,\n    + \t\t\t   !is_local);\n    + \n    +-\tupdate_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n    ++\tupdate_head(&opts, our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n    + \n    + \t/*\n    + \t * We want to show progress for recursive submodule clones iff\n     \n      ## parse-options.h ##\n     @@ parse-options.h: static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n    @@ t/meson.build: integration_tests = [\n        't5617-clone-submodules-remote.sh',\n        't5618-alternate-refs.sh',\n        't5619-clone-local-ambiguous-transport.sh',\n    -+  't5620-clone-revision.sh',\n    ++  't5621-clone-revision.sh',\n        't5700-protocol-v1.sh',\n        't5701-git-serve.sh',\n        't5702-protocol-v2.sh',\n     \n    - ## t/t5620-clone-revision.sh (new) ##\n    + ## t/t5621-clone-revision.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t5620-clone-revision.sh (new)\n     +\tgit clone --revision=refs/heads/feature . dst &&\n     +\tgit rev-parse refs/heads/feature >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n    ++\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n     +\ttest_cmp expect actual &&\n    -+\tgit for-each-ref refs/heads/feature > expect &&\n    -+\tgit -C dst for-each-ref > actual &&\n    -+\ttest_cmp expect actual\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch\n     +'\n     +\n     +test_expect_success 'clone with --depth and --revision being a branch' '\n     +\ttest_when_finished \"rm -rf dst\" &&\n    -+\tgit clone --depth=1 --revision=refs/heads/feature . dst &&\n    ++\tgit clone --no-local --depth=1 --revision=refs/heads/feature . dst &&\n     +\tgit rev-parse refs/heads/feature >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n    -+\ttest_cmp expect actual\n    ++\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch &&\n    ++\tgit -C dst rev-list HEAD >actual &&\n    ++\ttest_line_count = 1 actual\n     +'\n     +\n     +test_expect_success 'clone with --revision being a tag' '\n    @@ t/t5620-clone-revision.sh (new)\n     +\tgit clone --revision=refs/tags/v1.0 . dst &&\n     +\tgit rev-parse refs/tags/v1.0^{} >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n    -+\ttest_cmp expect actual\n    ++\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch\n     +'\n     +\n     +test_expect_success 'clone with --revision being HEAD' '\n    @@ t/t5620-clone-revision.sh (new)\n     +\tgit clone --revision=HEAD . dst &&\n     +\tgit rev-parse HEAD >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n    -+\ttest_cmp expect actual\n    ++\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch\n     +'\n     +\n     +test_expect_success 'clone with --revision being a raw commit hash' '\n    @@ t/t5620-clone-revision.sh (new)\n     +\tgit clone --revision=$oid . dst &&\n     +\techo $oid >expect &&\n     +\tgit -C dst rev-parse HEAD >actual &&\n    -+\ttest_cmp expect actual\n    ++\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch\n     +'\n     +\n     +test_expect_success 'clone with --revision and --bare' '\n     +\ttest_when_finished \"rm -rf dst\" &&\n     +\tgit clone --revision=refs/heads/main --bare . dst &&\n     +\toid=$(git rev-parse refs/heads/main) &&\n    -+\tgit -C dst cat-file -t $oid > actual &&\n    ++\tgit -C dst cat-file -t $oid >actual &&\n     +\techo \"commit\" >expect &&\n    -+\ttest_cmp expect actual\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C dst for-each-ref refs >expect &&\n    ++\ttest_must_be_empty expect &&\n    ++\ttest_must_fail git -C dst config remote.origin.fetch\n     +'\n     +\n     +test_expect_success 'clone with --revision being a short raw commit hash' '\n\n\n---\n\nbase-commit: 3b0d05c4a79d0e441283680a864529b02dca5f08\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nThanks\n--\nToon\n\n"},{"id":"511568","messageId":"20250131-toon-clone-refs-v4-1-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 1/6] clone: cut down on global variables in clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:29Z","receivedAt":"2025-01-31T15:31:05Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c the `struct option` which is used to parse the input options\nfor git-clone(1) is a global variable. Due to this, many variables that\nare used to parse the value into, are also global.\n\nMake `builtin_clone_options` a local variable in cmd_clone() and carry\nalong all variables that are only used in that function.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 195 +++++++++++++++++++++++++++++---------------------------\n 1 file changed, 101 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd001d800c635e46bbc7027a8fdda2a8c9fbf069..5ed0802f1d0ddebaf512aac93bf8c8b340494323 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -56,42 +56,22 @@\n  *  - dropping use-separate-remote and no-separate-remote compatibility\n  *\n  */\n-static const char * const builtin_clone_usage[] = {\n-\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n-\tNULL\n-};\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_no_tags;\n static int option_shallow_submodules;\n-static int option_reject_shallow = -1;    /* unspecified */\n static int config_reject_shallow = -1;    /* unspecified */\n-static int deepen;\n-static char *option_template, *option_depth, *option_since;\n-static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n-static struct string_list option_not = STRING_LIST_INIT_NODUP;\n-static const char *real_git_dir;\n-static const char *ref_format;\n-static const char *option_upload_pack = \"git-upload-pack\";\n static int option_verbosity;\n-static int option_progress = -1;\n-static int option_sparse_checkout;\n-static enum transport_family family;\n-static struct string_list option_config = STRING_LIST_INIT_NODUP;\n static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n-static int option_dissociate;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n-static int option_filter_submodules = -1;    /* unspecified */\n static int config_filter_submodules = -1;    /* unspecified */\n-static struct string_list server_options = STRING_LIST_INIT_NODUP;\n static int option_remote_submodules;\n-static const char *bundle_uri;\n \n static int recurse_submodules_cb(const struct option *opt,\n \t\t\t\t const char *arg, int unset)\n@@ -107,78 +87,6 @@ static int recurse_submodules_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static struct option builtin_clone_options[] = {\n-\tOPT__VERBOSITY(&option_verbosity),\n-\tOPT_BOOL(0, \"progress\", &option_progress,\n-\t\t N_(\"force progress reporting\")),\n-\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n-\t\t N_(\"don't clone shallow repository\")),\n-\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n-\t\t N_(\"don't create a checkout\")),\n-\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n-\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n-\t\t\tN_(\"create a bare repository\")),\n-\tOPT_BOOL(0, \"mirror\", &option_mirror,\n-\t\t N_(\"create a mirror repository (implies --bare)\")),\n-\tOPT_BOOL('l', \"local\", &option_local,\n-\t\tN_(\"to clone from a local repository\")),\n-\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n-\t\t    N_(\"don't use local hardlinks, always copy\")),\n-\tOPT_BOOL('s', \"shared\", &option_shared,\n-\t\t    N_(\"setup as shared repository\")),\n-\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n-\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules cloned in parallel\")),\n-\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n-\t\t   N_(\"directory from which templates will be used\")),\n-\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n-\t\t\tN_(\"reference repository\")),\n-\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n-\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n-\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n-\t\t N_(\"use --reference only while cloning\")),\n-\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n-\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n-\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n-\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n-\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to git-upload-pack on the remote\")),\n-\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n-\t\t    N_(\"create a shallow clone of that depth\")),\n-\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n-\t\t    N_(\"create a shallow clone since a specific time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n-\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n-\t\t    N_(\"clone only one branch, HEAD or --branch\")),\n-\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n-\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n-\t\t    N_(\"any cloned submodules will be shallow\")),\n-\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n-\t\t   N_(\"separate git dir from working tree\")),\n-\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n-\t\t   N_(\"specify the reference format to use\")),\n-\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n-\t\t\tN_(\"set config inside the new repository\")),\n-\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n-\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_IPVERSION(&family),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n-\t\t    N_(\"apply partial clone filters to submodules\")),\n-\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n-\t\t    N_(\"any cloned submodules will use their remote-tracking branch\")),\n-\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n-\t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n-\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n-\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n-\tOPT_END()\n-};\n-\n static const char *get_repo_path_1(struct strbuf *path, int *is_bundle)\n {\n \tstatic const char *suffix[] = { \"/.git\", \"\", \".git/.git\", \".git\" };\n@@ -989,10 +897,103 @@ int cmd_clone(int argc,\n \tint hash_algo;\n \tenum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n \tconst int do_not_override_repo_unix_permissions = -1;\n+\tint option_reject_shallow = -1; /* unspecified */\n+\tint deepen = 0;\n+\tchar *option_template = NULL, *option_depth = NULL, *option_since = NULL;\n+\tchar *option_origin = NULL;\n+\tstruct string_list option_not = STRING_LIST_INIT_NODUP;\n+\tconst char *real_git_dir = NULL;\n+\tconst char *ref_format = NULL;\n+\tconst char *option_upload_pack = \"git-upload-pack\";\n+\tint option_progress = -1;\n+\tint option_sparse_checkout = 0;\n+\tenum transport_family family = TRANSPORT_FAMILY_ALL;\n+\tstruct string_list option_config = STRING_LIST_INIT_DUP;\n+\tint option_dissociate = 0;\n+\tint option_filter_submodules = -1; /* unspecified */\n+\tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n+\tconst char *bundle_uri = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n+\tstruct option builtin_clone_options[] = {\n+\t\tOPT__VERBOSITY(&option_verbosity),\n+\t\tOPT_BOOL(0, \"progress\", &option_progress,\n+\t\t\t N_(\"force progress reporting\")),\n+\t\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n+\t\t\t N_(\"don't clone shallow repository\")),\n+\t\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n+\t\t\t N_(\"don't create a checkout\")),\n+\t\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n+\t\t\t\tN_(\"create a bare repository\")),\n+\t\tOPT_BOOL(0, \"mirror\", &option_mirror,\n+\t\t\t N_(\"create a mirror repository (implies --bare)\")),\n+\t\tOPT_BOOL('l', \"local\", &option_local,\n+\t\t\t N_(\"to clone from a local repository\")),\n+\t\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n+\t\t\t N_(\"don't use local hardlinks, always copy\")),\n+\t\tOPT_BOOL('s', \"shared\", &option_shared,\n+\t\t\t N_(\"setup as shared repository\")),\n+\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n+\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n+\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules cloned in parallel\")),\n+\t\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n+\t\t\t   N_(\"directory from which templates will be used\")),\n+\t\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n+\t\t\t\tN_(\"reference repository\")),\n+\t\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n+\t\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n+\t\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n+\t\t\t N_(\"use --reference only while cloning\")),\n+\t\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n+\t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n+\t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to git-upload-pack on the remote\")),\n+\t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n+\t\t\t   N_(\"create a shallow clone of that depth\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n+\t\t\t   N_(\"create a shallow clone since a specific time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n+\t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n+\t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n+\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n+\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n+\t\t\t N_(\"any cloned submodules will be shallow\")),\n+\t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n+\t\t\t   N_(\"separate git dir from working tree\")),\n+\t\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n+\t\t\t   N_(\"specify the reference format to use\")),\n+\t\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n+\t\t\t\tN_(\"set config inside the new repository\")),\n+\t\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_IPVERSION(&family),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n+\t\t\t N_(\"apply partial clone filters to submodules\")),\n+\t\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n+\t\t\t N_(\"any cloned submodules will use their remote-tracking branch\")),\n+\t\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n+\t\t\t N_(\"initialize sparse-checkout file to include only files at root\")),\n+\t\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n+\t\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tconst char * const builtin_clone_usage[] = {\n+\t\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n+\t\tNULL\n+\t};\n+\n \tpacket_trace_identity(\"clone\");\n \n \tgit_config(git_clone_config, NULL);\n@@ -1138,8 +1139,8 @@ int cmd_clone(int argc,\n \t\tfor_each_string_list_item(item, &option_recurse_submodules) {\n \t\t\tstrbuf_addf(&sb, \"submodule.active=%s\",\n \t\t\t\t    item->string);\n-\t\t\tstring_list_append(&option_config,\n-\t\t\t\t\t   strbuf_detach(&sb, NULL));\n+\t\t\tstring_list_append(&option_config, sb.buf);\n+\t\t\tstrbuf_reset(&sb);\n \t\t}\n \n \t\tif (!git_config_get_bool(\"submodule.stickyRecursiveClone\", &val) &&\n@@ -1161,6 +1162,8 @@ int cmd_clone(int argc,\n \t\t\tstring_list_append(&option_config,\n \t\t\t\t\"submodule.alternateErrorStrategy=info\");\n \t\t}\n+\n+\t\tstrbuf_release(&sb);\n \t}\n \n \t/*\n@@ -1578,6 +1581,10 @@ int cmd_clone(int argc,\n \terr = checkout(submodule_progress, filter_submodules,\n \t\t       ref_storage_format);\n \n+\tstring_list_clear(&option_not, 0);\n+\tstring_list_clear(&option_config, 0);\n+\tstring_list_clear(&server_options, 0);\n+\n \tfree(remote_name);\n \tstrbuf_release(&reflog_msg);\n \tstrbuf_release(&branch_top);\n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511569","messageId":"20250131-toon-clone-refs-v4-2-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 2/6] clone: make it possible to specify --tags","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:30Z","receivedAt":"2025-01-31T15:31:09Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --no-tags was added in 0dab2468ee (clone: add a --no-tags option\nto clone without tags, 2017-04-26). At the time there was no need to\nsupport --tags as well, although there was some conversation about\nit[1].\n\nTo simplify the code and to prepare for future commits, invert the flag\ninternally. Functionally there is no change, because the flag is\ndefault-enabled passing `--tags` has no effect, so there's no need to\nadd tests for this.\n\n[1]: https://lore.kernel.org/git/CAGZ79kbHuMpiavJ90kQLEL_AR0BEyArcZoEWAjPPhOFacN16YQ@mail.gmail.com/\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |  7 ++++---\n builtin/clone.c             | 14 +++++++-------\n 2 files changed, 11 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex de8d8f58930ecff305f79480b13ddce10cd96c60..99a9222e63429b3447deb3e7c08962d4ec60a295 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -13,7 +13,7 @@ git clone [--template=<template-directory>]\n \t  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]\n \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]\n \t  [--dissociate] [--separate-git-dir <git-dir>]\n-\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n+\t  [--depth <depth>] [--[no-]single-branch] [--[no-]-tags]\n \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]\n \t  [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository>\n@@ -273,8 +273,9 @@ corresponding `--mirror` and `--no-tags` options instead.\n \tbranch when `--single-branch` clone was made, no remote-tracking\n \tbranch is created.\n \n-`--no-tags`::\n-\tDon't clone any tags, and set\n+`--[no-]tags`::\n+\tBy default tags are cloned, and passing `--tags` doesn't change that.\n+\tWith `--no-tags`, no tags are cloned, and set\n \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n \tthat future `git pull` and `git fetch` operations won't follow\n \tany tags. Subsequent explicit tag fetches will still work,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5ed0802f1d0ddebaf512aac93bf8c8b340494323..69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,7 +59,7 @@\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n-static int option_no_tags;\n+static int option_tags = 1; /* default enabled */\n static int option_shallow_submodules;\n static int config_reject_shallow = -1;    /* unspecified */\n static char *remote_name = NULL;\n@@ -470,7 +470,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && !option_no_tags)\n+\tif (!option_mirror && !option_single_branch && option_tags)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n \trefspec_item_clear(&tag_refspec);\n@@ -562,7 +562,7 @@ static void update_remote_refs(const struct ref *refs,\n \n \tif (refs) {\n \t\twrite_remote_refs(mapped_refs);\n-\t\tif (option_single_branch && !option_no_tags)\n+\t\tif (option_single_branch && option_tags)\n \t\t\twrite_followtags(refs, msg);\n \t}\n \n@@ -964,8 +964,8 @@ int cmd_clone(int argc,\n \t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n \t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n \t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n-\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"tags\", &option_tags,\n+\t\t\t N_(\"clone tags, and make later fetches not to follow them\")),\n \t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n \t\t\t N_(\"any cloned submodules will be shallow\")),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n@@ -1296,7 +1296,7 @@ int cmd_clone(int argc,\n \tgit_config_set(key.buf, repo);\n \tstrbuf_reset(&key);\n \n-\tif (option_no_tags) {\n+\tif (!option_tags) {\n \t\tstrbuf_addf(&key, \"remote.%s.tagOpt\", remote_name);\n \t\tgit_config_set(key.buf, \"--no-tags\");\n \t\tstrbuf_reset(&key);\n@@ -1389,7 +1389,7 @@ int cmd_clone(int argc,\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (!option_no_tags)\n+\tif (option_tags)\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511570","messageId":"20250131-toon-clone-refs-v4-3-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 3/6] clone: refactor wanted_peer_refs()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:31Z","receivedAt":"2025-01-31T15:31:17Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The function wanted_peer_refs() is used to map the refs returned by the\nserver to refs we will save in our clone.\n\nOver time this function grown to be very complex. Refactor it.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 39 +++++++++++++++------------------------\n 1 file changed, 15 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4..d652682494d0d27dd73cd0585e28b23f2883786d 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -434,46 +434,37 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n {\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n-\tstruct ref **tail = head ? &head->next : &local_refs;\n+\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n \tstruct refspec_item tag_refspec;\n+\tstruct ref *to_free = NULL;\n \n \trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n \n \tif (option_single_branch) {\n-\t\tstruct ref *remote_head = NULL;\n-\n \t\tif (!option_branch)\n-\t\t\tremote_head = guess_remote_head(head, refs, 0);\n+\t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n \t\telse {\n \t\t\tfree_one_ref(head);\n \t\t\tlocal_refs = head = NULL;\n \t\t\ttail = &local_refs;\n-\t\t\tremote_head = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n-\n-\t\tif (!remote_head && option_branch)\n-\t\t\twarning(_(\"Could not find remote branch %s to clone.\"),\n-\t\t\t\toption_branch);\n-\t\telse {\n-\t\t\tint i;\n-\t\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\t\tget_fetch_map(remote_head, &refspec->items[i],\n-\t\t\t\t\t      &tail, 0);\n-\n-\t\t\t/* if --branch=tag, pull the requested tag explicitly */\n-\t\t\tget_fetch_map(remote_head, &tag_refspec, &tail, 0);\n+\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t\t}\n-\t\tfree_refs(remote_head);\n-\t} else {\n-\t\tint i;\n-\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && option_tags)\n+\tfor (int i = 0; i < refspec->nr; i++)\n+\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n+\n+\t/*\n+\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n+\t * about won't be present in `refs` anyway.\n+\t * Except with option --mirror, where we grab all refs already.\n+\t */\n+\tif (!option_mirror)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n+\tfree_one_ref(to_free);\n \trefspec_item_clear(&tag_refspec);\n+\n \treturn local_refs;\n }\n \n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511571","messageId":"20250131-toon-clone-refs-v4-4-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 4/6] clone: add tags refspec earlier to fetch refspec","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:32Z","receivedAt":"2025-01-31T15:31:25Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\nfrom the `remote->fetch` refspec into `ref_prefixes` of\n`transport_ls_refs_options`. Afterward we add the tags prefix\n`refs/tags/` prefix as well. At a later point, in wanted_peer_refs() we\nprocess refs using both `remote->fetch` and `TAG_REFSPEC`.\n\nSimplify the code by appending `TAG_REFSPEC` to `remote->fetch` before\ncalling refspec_ref_prefixes().\n\nTo be able to do this, we set `option_tags` to 0 when --mirror is given.\nThis is because --mirror mirrors (hence the name) all the refs,\nincluding tags and they do not need to be treated separately.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 23 +++++++----------------\n 1 file changed, 7 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex d652682494d0d27dd73cd0585e28b23f2883786d..7ab156ac00240de89baca6533ed2541839286fc4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -435,11 +435,8 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n \tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n-\tstruct refspec_item tag_refspec;\n \tstruct ref *to_free = NULL;\n \n-\trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n-\n \tif (option_single_branch) {\n \t\tif (!option_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n@@ -454,16 +451,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tfor (int i = 0; i < refspec->nr; i++)\n \t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \n-\t/*\n-\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n-\t * about won't be present in `refs` anyway.\n-\t * Except with option --mirror, where we grab all refs already.\n-\t */\n-\tif (!option_mirror)\n-\t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n-\n \tfree_one_ref(to_free);\n-\trefspec_item_clear(&tag_refspec);\n \n \treturn local_refs;\n }\n@@ -1011,8 +999,10 @@ int cmd_clone(int argc,\n \t\t\tdie(_(\"unknown ref storage format '%s'\"), ref_format);\n \t}\n \n-\tif (option_mirror)\n+\tif (option_mirror) {\n \t\toption_bare = 1;\n+\t\toption_tags = 0;\n+\t}\n \n \tif (option_bare) {\n \t\tif (real_git_dir)\n@@ -1375,14 +1365,15 @@ int cmd_clone(int argc,\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n+\tif (option_tags || option_branch)\n+\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n+\n \trefspec_ref_prefixes(&remote->fetch,\n \t\t\t     &transport_ls_refs_options.ref_prefixes);\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (option_tags)\n-\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n-\t\t\t    \"refs/tags/\");\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511572","messageId":"20250131-toon-clone-refs-v4-5-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 5/6] clone: introduce struct clone_opts in builtin/clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:33Z","receivedAt":"2025-01-31T15:31:32Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"There is a lot of state stored in global variables in builtin/clone.c.\nIn the long run we'd like to remove many of those.\n\nIntroduce `struct clone_opts` in this file. This struct will be used to\ncontain all details needed to perform the clone. The struct object can\nbe thrown around to all the functions that need these details.\n\nThe first field we're adding is `wants_head`. In some scenarios\n(specifically when both `--single-branch` and `--branch` are given) we\nare not interested in `HEAD` on the remote. The field `wants_head` in\n`struct clone_opts` will hold this information. We could have put\n`option_branch` and `option_single_branch` into that struct instead, but\nin a following commit we'll be using `wants_head` as well.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 47 ++++++++++++++++++++++++++++++++---------------\n remote.c        |  2 +-\n remote.h        |  1 +\n 3 files changed, 34 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 7ab156ac00240de89baca6533ed2541839286fc4..f92017c751dd31cb25a3ba31667b015d5766ce84 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -57,6 +57,13 @@\n  *\n  */\n \n+struct clone_opts {\n+\tint wants_head;\n+};\n+#define CLONE_OPTS_INIT { \\\n+\t.wants_head = 1 /* default enabled */ \\\n+}\n+\n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_tags = 1; /* default enabled */\n@@ -429,23 +436,27 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch\n \treturn ref;\n }\n \n-static struct ref *wanted_peer_refs(const struct ref *refs,\n-\t\tstruct refspec *refspec)\n+static struct ref *wanted_peer_refs(struct clone_opts *opts,\n+\t\t\t\t    const struct ref *refs,\n+\t\t\t\t    struct refspec *refspec)\n {\n-\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n-\tstruct ref *local_refs = head;\n-\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n+\tstruct ref *local_refs = NULL;\n+\tstruct ref **tail = &local_refs;\n \tstruct ref *to_free = NULL;\n \n-\tif (option_single_branch) {\n-\t\tif (!option_branch)\n+\tif (opts->wants_head) {\n+\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n+\t\tif (head)\n+\t\t\ttail_link_ref(head, &tail);\n+\n+\t\tif (option_single_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n-\t\telse {\n-\t\t\tfree_one_ref(head);\n-\t\t\tlocal_refs = head = NULL;\n-\t\t\ttail = &local_refs;\n-\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n+\t}\n+\n+\telse if (option_single_branch) {\n+\t\tlocal_refs = NULL;\n+\t\ttail = &local_refs;\n+\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t}\n \n \tfor (int i = 0; i < refspec->nr; i++)\n@@ -893,6 +904,8 @@ int cmd_clone(int argc,\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n \n+\tstruct clone_opts opts = CLONE_OPTS_INIT;\n+\n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n@@ -1343,9 +1356,13 @@ int cmd_clone(int argc,\n \tif (option_not.nr)\n \t\ttransport_set_option(transport, TRANS_OPT_DEEPEN_NOT,\n \t\t\t\t     (const char *)&option_not);\n-\tif (option_single_branch)\n+\tif (option_single_branch) {\n \t\ttransport_set_option(transport, TRANS_OPT_FOLLOWTAGS, \"1\");\n \n+\t\tif (option_branch)\n+\t\t\topts.wants_head = 0;\n+\t}\n+\n \tif (option_upload_pack)\n \t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK,\n \t\t\t\t     option_upload_pack);\n@@ -1450,7 +1467,7 @@ int cmd_clone(int argc,\n \t}\n \n \tif (refs)\n-\t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t\tmapped_refs = wanted_peer_refs(&opts, refs, &remote->fetch);\n \n \tif (mapped_refs) {\n \t\t/*\ndiff --git a/remote.c b/remote.c\nindex 0f6fba85625b523122e50e28a0f64b6e143cd9fb..cf823764826222bab84d27ae665f7292da44edc6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1234,7 +1234,7 @@ int count_refspec_match(const char *pattern,\n \t}\n }\n \n-static void tail_link_ref(struct ref *ref, struct ref ***tail)\n+void tail_link_ref(struct ref *ref, struct ref ***tail)\n {\n \t**tail = ref;\n \twhile (ref->next)\ndiff --git a/remote.h b/remote.h\nindex bda10dd5c85ffd8988a6c3d39583e7b9701278b8..65d2de2c2051cf28def8b43fcca3fd5e1c86a0d8 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -219,6 +219,7 @@ struct ref *alloc_ref(const char *name);\n struct ref *copy_ref(const struct ref *ref);\n struct ref *copy_ref_list(const struct ref *ref);\n int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n+void tail_link_ref(struct ref *ref, struct ref ***tail);\n \n int check_ref_type(const struct ref *ref, int flags);\n \n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511573","messageId":"20250131-toon-clone-refs-v4-6-2a4ff851498f@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v4 6/6] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-01-31T15:30:34Z","receivedAt":"2025-01-31T15:31:37Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot be selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a hexadecimal commit ID. This enables the user\nto clone and check out any revision they want. `--revision` can be used\nin conjunction with `--depth` to do a minimal clone that only contains\nthe blob and tree for a single revision. This can be useful for\nautomated tests running in CI systems.\n\nUsing option `--branch` and `--single-branch` together is a similar\nscenario, but serves a different purpose. Using these two options, a\nsinglet remote tracking branch is created and the fetch refspec is set\nup so git-fetch(1) will receive updates on that branch from the remote.\nThis allows the user work on that single branch.\n\nOption `--revision` on contrary detaches HEAD, creates no tracking\nbranches, and writes no fetch refspec.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |  10 ++++\n builtin/clone.c             |  59 +++++++++++++++++----\n parse-options.h             |   9 ++++\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 193 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex 99a9222e63429b3447deb3e7c08962d4ec60a295..6c882b6fc56c2595059124cd0ecdaf825e310160 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -221,6 +221,16 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision` _<rev>_::\n+\tCreate a new repository, and fetch the history leading to the given\n+\trevision _<rev>_ (and nothing else), without making any remote-tracking\n+\tbranch, and without making any local branch, and point `HEAD` to\n+\t_<rev>_. When creating a non-bare repository, the revision is checked\n+\tout on a detached `HEAD`. The argument can be a ref name\n+\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n+\tcommit, or a hexadecimal object name.\n+\tThis option is incompatible with `--branch` and `--mirror`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex f92017c751dd31cb25a3ba31667b015d5766ce84..40d6ecfa74608193a88715a29b4ca765687a0c86 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,6 +59,7 @@\n \n struct clone_opts {\n \tint wants_head;\n+\tint detach;\n };\n #define CLONE_OPTS_INIT { \\\n \t.wants_head = 1 /* default enabled */ \\\n@@ -568,11 +569,11 @@ static void update_remote_refs(const struct ref *refs,\n \t}\n }\n \n-static void update_head(const struct ref *our, const struct ref *remote,\n+static void update_head(struct clone_opts *opts, const struct ref *our, const struct ref *remote,\n \t\t\tconst char *unborn, const char *msg)\n {\n \tconst char *head;\n-\tif (our && skip_prefix(our->name, \"refs/heads/\", &head)) {\n+\tif (our && !opts->detach && skip_prefix(our->name, \"refs/heads/\", &head)) {\n \t\t/* Local default branch link */\n \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n \t\t\tdie(_(\"unable to update HEAD\"));\n@@ -585,6 +586,10 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t} else if (our) {\n \t\tstruct commit *c = lookup_commit_reference(the_repository,\n \t\t\t\t\t\t\t   &our->old_oid);\n+\n+\t\tif (!c)\n+\t\t\tdie(_(\"couldn't look up commit object for '%s'\"), our->name);\n+\n \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n@@ -903,6 +908,7 @@ int cmd_clone(int argc,\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n+\tchar *option_rev = NULL;\n \n \tstruct clone_opts opts = CLONE_OPTS_INIT;\n \n@@ -946,6 +952,8 @@ int cmd_clone(int argc,\n \t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t\t   N_(\"clone single revision <rev> and check out\")),\n \t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t\t   N_(\"path to git-upload-pack on the remote\")),\n \t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -1282,7 +1290,7 @@ int cmd_clone(int argc,\n \t\tstrbuf_addstr(&branch_top, src_ref_prefix);\n \n \t\tgit_config_set(\"core.bare\", \"true\");\n-\t} else {\n+\t} else if (!option_rev) {\n \t\tstrbuf_addf(&branch_top, \"refs/remotes/%s/\", remote_name);\n \t}\n \n@@ -1301,8 +1309,9 @@ int cmd_clone(int argc,\n \n \tremote = remote_get_early(remote_name);\n \n-\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n-\t\t\tbranch_top.buf);\n+\tif (!option_rev)\n+\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n+\t\t\t\tbranch_top.buf);\n \n \tpath = get_repo_path(remote->url.v[0], &is_bundle);\n \tis_local = option_local != 0 && path && !is_bundle;\n@@ -1345,6 +1354,12 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  !!option_branch, \"--branch\");\n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  option_mirror, \"--mirror\");\n+\t// TODO --no-single-branch\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1381,7 +1396,15 @@ int cmd_clone(int argc,\n \tif (transport->smart_options && !deepen && !filter_options.choice)\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n-\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\tif (option_rev) {\n+\t\toption_tags = 0;\n+\t\toption_branch = 0;\n+\t\toption_single_branch = 0;\n+\t\topts.wants_head = 0;\n+\t\topts.detach = 1;\n+\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\t}\n \n \tif (option_tags || option_branch)\n \t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n@@ -1392,6 +1415,17 @@ int cmd_clone(int argc,\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n \n+\t/*\n+\t * As part of transport_get_remote_refs() the server tells us the hash\n+\t * algorithm, which we require to initialize the repo. But calling that\n+\t * function without any ref prefix, will cause the server to announce\n+\t * all known refs. If the argument passed to --revision was a hex oid,\n+\t * ref_prefixes will be empty so we fall back to asking about HEAD to\n+\t * reduce traffic from the server.\n+\t */\n+\tif (opts.wants_head || transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n \t/*\n@@ -1500,6 +1534,12 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n+\t\tif (!our_head_points_at)\n+\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n+\t\t\t    option_rev, remote_name);\n+\t\t//mapped_refs->name[0] = 0;\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\n@@ -1538,8 +1578,9 @@ int cmd_clone(int argc,\n \t\tfree(to_free);\n \t}\n \n-\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n-\t\t\tremote_head_points_at, &branch_top);\n+\tif (!option_rev)\n+\t\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n+\t\t\t\t     remote_head_points_at, &branch_top);\n \n \tif (filter_options.choice)\n \t\tpartial_clone_register(remote_name, &filter_options);\n@@ -1555,7 +1596,7 @@ int cmd_clone(int argc,\n \t\t\t   branch_top.buf, reflog_msg.buf, transport,\n \t\t\t   !is_local);\n \n-\tupdate_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n+\tupdate_head(&opts, our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n \n \t/*\n \t * We want to show progress for recursive submodule clones iff\ndiff --git a/parse-options.h b/parse-options.h\nindex 39f088625494f20dea96b9a9cbe986916773bf60..fca944d9a93d643d984c58de2ead9154c8b16c94 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -436,6 +436,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n \t\t\t\t  0, \"\");\n }\n \n+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n+\t\t\t\t\t     int opt2, const char *opt2_name)\n+{\n+\tdie_for_incompatible_opt4(opt1, opt1_name,\n+\t\t\t\t  opt2, opt2_name,\n+\t\t\t\t  0, \"\",\n+\t\t\t\t  0, \"\");\n+}\n+\n /*\n  * Use these assertions for callbacks that expect to be called with NONEG and\n  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\ndiff --git a/t/meson.build b/t/meson.build\nindex 35f25ca4a1d960564190288e9456620a46ccc80a..b5f917926b61de379b6cef45e5f750912422a7d1 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -721,6 +721,7 @@ integration_tests = [\n   't5617-clone-submodules-remote.sh',\n   't5618-alternate-refs.sh',\n   't5619-clone-local-ambiguous-transport.sh',\n+  't5621-clone-revision.sh',\n   't5700-protocol-v1.sh',\n   't5701-git-serve.sh',\n   't5702-protocol-v2.sh',\ndiff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n--- /dev/null\n+++ b/t/t5621-clone-revision.sh\n@@ -0,0 +1,123 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --no-local --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch &&\n+\tgit -C dst rev-list HEAD >actual &&\n+\ttest_line_count = 1 actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0^{} >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision and --bare' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/main --bare . dst &&\n+\toid=$(git rev-parse refs/heads/main) &&\n+\tgit -C dst cat-file -t $oid >actual &&\n+\techo \"commit\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"error: object $oid is a tree, not a commit\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_done\n\n-- \n2.48.1.164.g9a5474a648.dirty\n\n"},{"id":"511595","messageId":"xmqq1pwi7kyv.fsf@gitster.g","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-6-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 6/6] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-01-31T21:05:28Z","receivedAt":"2025-01-31T21:05:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n>  \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n>  \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n> +\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n> +\t\t\t   N_(\"clone single revision <rev> and check out\")),\n\nOK, this thing comes as a string; we'll parse it down to a commit\nlater, hopefully?\n\n> -\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n> -\t\t\tbranch_top.buf);\n> +\tif (!option_rev)\n> +\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n> +\t\t\t\tbranch_top.buf);\n\n> +\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n> +\t\t\t\t  !!option_branch, \"--branch\");\n> +\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n> +\t\t\t\t  option_mirror, \"--mirror\");\n\nSo here is where we mark the new thing incompatible with these two,\nand when either of them is given with \"--revision\", we would bail\nout.  OK.\n\n> +\t// TODO --no-single-branch\n\nStyle.\n\n> @@ -1381,7 +1396,15 @@ int cmd_clone(int argc,\n>  \tif (transport->smart_options && !deepen && !filter_options.choice)\n>  \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n>  \n> -\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n> +\tif (option_rev) {\n> +\t\toption_tags = 0;\n> +\t\toption_branch = 0;\n> +\t\toption_single_branch = 0;\n> +\t\topts.wants_head = 0;\n> +\t\topts.detach = 1;\n\noption_branch is of type \"char *\" so sparse rightfully complains\nthat you are assigning an integer 0 to it, which follows stronger\nrules than plain vanilla C standard to help us avoid mistakes.\n\nBut stepping back a bit, hasn't we already been ruled out earlier\nthat when option_rev is set, we cannot possibly be affected by the\n\"--branch\" option that was given at the same time?  I do not now\nabout other assignments we see in this block, but are there others\nthat are unnecessary?  For example, you do not clear option_mirror\nin this block.  Is option_branch so special that it needs clearing,\nand if so why?\n\nThanks.\n"},{"id":"511638","messageId":"1911002.tdWV9SEqCh@cayenne","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-2-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 2/6] clone: make it possible to specify --tags","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-02-01T16:47:36Z","receivedAt":"2025-02-01T16:47:57Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Hello,\n\nI'm only reviewing the doc part.\n\nOn Friday, 31 January 2025 16:30:30 UTC+1 Toon Claes wrote:\n> Option --no-tags was added in 0dab2468ee (clone: add a --no-tags option\n> to clone without tags, 2017-04-26). At the time there was no need to\n> support --tags as well, although there was some conversation about\n> it[1].\n> \n> To simplify the code and to prepare for future commits, invert the flag\n> internally. Functionally there is no change, because the flag is\n> default-enabled passing `--tags` has no effect, so there's no need to\n> add tests for this.\n> \n> [1]:\n> https://lore.kernel.org/git/\nCAGZ79kbHuMpiavJ90kQLEL_AR0BEyArcZoEWAjPPhOFacN16\n> YQ@mail.gmail.com/\n> \n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-clone.txt |  7 ++++---\n>  builtin/clone.c             | 14 +++++++-------\n>  2 files changed, 11 insertions(+), 10 deletions(-)\n> \n> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> index\n> de8d8f58930ecff305f79480b13ddce10cd96c60..99a9222e63429b3447deb3e7c08962d4ec6\n> 0a295 100644 --- a/Documentation/git-clone.txt\n> +++ b/Documentation/git-clone.txt\n> @@ -13,7 +13,7 @@ git clone [--template=<template-directory>]\n>  \t  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]\n>  \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference \n<repository>]\n>  \t  [--dissociate] [--separate-git-dir <git-dir>]\n> -\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n> +\t  [--depth <depth>] [--[no-]single-branch] [--[no-]-tags]\n\nThere's an extra '-' : it should read '--[no-]tags'\n\n>  \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n>  \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--\n[no-]reject-shallow]\n> [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository> @@\n> -273,8 +273,9 @@ corresponding `--mirror` and `--no-tags` options instead.\n> branch when `--single-branch` clone was made, no remote-tracking\n>  \tbranch is created.\n> \n> -`--no-tags`::\n> -\tDon't clone any tags, and set\n> +`--[no-]tags`::\n> +\tBy default tags are cloned, and passing `--tags` doesn't change \nthat.\n> +\tWith `--no-tags`, no tags are cloned, and set\n\nBetter keep and start the description  with the imperative mood , as in the \nprevious version and add the \"By default,...\" at the end of the description.\n\n>  \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n>  \tthat future `git pull` and `git fetch` operations won't follow\n>  \tany tags. Subsequent explicit tag fetches will still work,\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index\n\n\n\n"},{"id":"511639","messageId":"2974411.e9J7NaK4W3@cayenne","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-6-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 6/6] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-02-01T16:50:36Z","receivedAt":"2025-02-01T16:50:44Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Friday, 31 January 2025 16:30:34 UTC+1 Toon Claes wrote:\n> The git-clone(1) command has the option `--branch` that allows the user\n> to select the branch they want HEAD to point to. In a non-bare\n> repository this also checks out that branch.\n> \n> Option `--branch` also accepts a tag. When a tag name is provided, the\n> commit this tag points to is checked out and HEAD is detached. Thus\n> `--branch` can be used to clone a repository and check out a ref kept\n> under `refs/heads` or `refs/tags`. But some other refs might be in use\n> as well. For example Git forges might use refs like `refs/pull/<id>` and\n> `refs/merge-requests/<id>` to track pull/merge requests. These refs\n> cannot be selected upon git-clone(1).\n> \n> Add option `--revision` to git-clone(1). This option accepts a fully\n> qualified reference, or a hexadecimal commit ID. This enables the user\n> to clone and check out any revision they want. `--revision` can be used\n> in conjunction with `--depth` to do a minimal clone that only contains\n> the blob and tree for a single revision. This can be useful for\n> automated tests running in CI systems.\n> \n> Using option `--branch` and `--single-branch` together is a similar\n> scenario, but serves a different purpose. Using these two options, a\n> singlet remote tracking branch is created and the fetch refspec is set\n> up so git-fetch(1) will receive updates on that branch from the remote.\n> This allows the user work on that single branch.\n> \n> Option `--revision` on contrary detaches HEAD, creates no tracking\n> branches, and writes no fetch refspec.\n> \n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-clone.txt |  10 ++++\n>  builtin/clone.c             |  59 +++++++++++++++++----\n>  parse-options.h             |   9 ++++\n>  t/meson.build               |   1 +\n>  t/t5621-clone-revision.sh   | 123\n> ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 193\n> insertions(+), 9 deletions(-)\n> \n> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> index\n> \n99a9222e63429b3447deb3e7c08962d4ec60a295..6c882b6fc56c2595059124cd0ecdaf825e3\n> 10160 100644 --- a/Documentation/git-clone.txt\n> +++ b/Documentation/git-clone.txt\n> @@ -221,6 +221,16 @@ objects from the source repository into a pack in the\n> cloned repository. `--branch` can also take tags and detaches the `HEAD` at\n> that commit in the resulting repository.\n> \n> +`--revision` _<rev>_::\n\nYou can use the backticks for full synopsis style: `--revision <rev>`\n\n> +\tCreate a new repository, and fetch the history leading to the given\n> +\trevision _<rev>_ (and nothing else), without making any remote-\ntracking\n> +\tbranch, and without making any local branch, and point `HEAD` to\n> +\t_<rev>_. When creating a non-bare repository, the revision is \nchecked\n> +\tout on a detached `HEAD`. The argument can be a ref name\n> +\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n> +\tcommit, or a hexadecimal object name.\n> +\tThis option is incompatible with `--branch` and `--mirror`.\n> +\n>  `-u` _<upload-pack>_::\n>  `--upload-pack` _<upload-pack>_::\n>  \tWhen given, and the repository to clone from is accessed\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index\n\n\n\n"},{"id":"511691","messageId":"Z6B1c1lrmK3RYzKi@pks.im","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-3-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 3/6] clone: refactor wanted_peer_refs()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-03T07:51:15Z","receivedAt":"2025-02-03T07:51:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 31, 2025 at 04:30:31PM +0100, Toon Claes wrote:\n> The function wanted_peer_refs() is used to map the refs returned by the\n> server to refs we will save in our clone.\n> \n> Over time this function grown to be very complex. Refactor it.\n\nThe diff of this commit is a bit on the harder side to read, so it would\nbe nice if the message guided the reader a bit.\n\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  builtin/clone.c | 39 +++++++++++++++------------------------\n>  1 file changed, 15 insertions(+), 24 deletions(-)\n> \n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4..d652682494d0d27dd73cd0585e28b23f2883786d 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -434,46 +434,37 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n[snip]\n> -\tif (!option_mirror && !option_single_branch && option_tags)\n> +\tfor (int i = 0; i < refspec->nr; i++)\n\nWhile at it: this should be `size_t i` to match the type of\n`refspec->nr`.\n\nPatrick\n"},{"id":"511692","messageId":"Z6B1f1jB8KoTp8P8@pks.im","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-4-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 4/6] clone: add tags refspec earlier to fetch refspec","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-03T07:51:27Z","receivedAt":"2025-02-03T07:51:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 31, 2025 at 04:30:32PM +0100, Toon Claes wrote:\n> In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\n> from the `remote->fetch` refspec into `ref_prefixes` of\n> `transport_ls_refs_options`. Afterward we add the tags prefix\n\ns/Afterward/&s/\n\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index d652682494d0d27dd73cd0585e28b23f2883786d..7ab156ac00240de89baca6533ed2541839286fc4 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -1375,14 +1365,15 @@ int cmd_clone(int argc,\n>  \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n>  \n>  \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n> +\n> +\tif (option_tags || option_branch)\n> +\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n\nIt's a bit surprising that we also do this with `option_branch`, which\nonly seems to indicate which branch git-clone(1) is supposed to check\nout. But in fact, the documentation mentions that it may also be used to\ncheck out a tag. Principle of least surprise at its best.\n\nIn any case, I think it would be nice to have a comment here explaining\nwhy this is the correct thing to do.\n\nPatrick\n"},{"id":"511693","messageId":"Z6B1g1bkUpU78NUw@pks.im","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-5-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 5/6] clone: introduce struct clone_opts in builtin/clone.c","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-03T07:51:31Z","receivedAt":"2025-02-03T07:51:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 31, 2025 at 04:30:33PM +0100, Toon Claes wrote:\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 7ab156ac00240de89baca6533ed2541839286fc4..f92017c751dd31cb25a3ba31667b015d5766ce84 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -429,23 +436,27 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch\n>  \treturn ref;\n>  }\n>  \n> -static struct ref *wanted_peer_refs(const struct ref *refs,\n> -\t\tstruct refspec *refspec)\n> +static struct ref *wanted_peer_refs(struct clone_opts *opts,\n> +\t\t\t\t    const struct ref *refs,\n> +\t\t\t\t    struct refspec *refspec)\n>  {\n> -\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n> -\tstruct ref *local_refs = head;\n> -\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n> +\tstruct ref *local_refs = NULL;\n> +\tstruct ref **tail = &local_refs;\n>  \tstruct ref *to_free = NULL;\n>  \n> -\tif (option_single_branch) {\n> -\t\tif (!option_branch)\n> +\tif (opts->wants_head) {\n> +\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n> +\t\tif (head)\n> +\t\t\ttail_link_ref(head, &tail);\n> +\n> +\t\tif (option_single_branch)\n>  \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n> -\t\telse {\n> -\t\t\tfree_one_ref(head);\n> -\t\t\tlocal_refs = head = NULL;\n> -\t\t\ttail = &local_refs;\n> -\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n> -\t\t}\n> +\t}\n> +\n> +\telse if (option_single_branch) {\n\nFormatting: the `else if` should be on the same line as the closing\nbrace.\n\n> +\t\tlocal_refs = NULL;\n> +\t\ttail = &local_refs;\n> +\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n>  \t}\n>  \n>  \tfor (int i = 0; i < refspec->nr; i++)\n> @@ -893,6 +904,8 @@ int cmd_clone(int argc,\n>  \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n>  \tconst char *bundle_uri = NULL;\n>  \n> +\tstruct clone_opts opts = CLONE_OPTS_INIT;\n> +\n\nNit: There's a bit too many empty lines to my taste, without a clear\nreason why.\n\n>  \tstruct transport_ls_refs_options transport_ls_refs_options =\n>  \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n>  \n> @@ -1343,9 +1356,13 @@ int cmd_clone(int argc,\n>  \tif (option_not.nr)\n>  \t\ttransport_set_option(transport, TRANS_OPT_DEEPEN_NOT,\n>  \t\t\t\t     (const char *)&option_not);\n> -\tif (option_single_branch)\n> +\tif (option_single_branch) {\n>  \t\ttransport_set_option(transport, TRANS_OPT_FOLLOWTAGS, \"1\");\n>  \n> +\t\tif (option_branch)\n> +\t\t\topts.wants_head = 0;\n\nMakes sense. If we only want to clone a single branch, and we specify\nwhich branch that is, then there is no need to fetch the remote HEAD.\n\n> +\t}\n> +\n>  \tif (option_upload_pack)\n>  \t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK,\n>  \t\t\t\t     option_upload_pack);\n> @@ -1450,7 +1467,7 @@ int cmd_clone(int argc,\n>  \t}\n>  \n>  \tif (refs)\n> -\t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n> +\t\tmapped_refs = wanted_peer_refs(&opts, refs, &remote->fetch);\n>  \n>  \tif (mapped_refs) {\n>  \t\t/*\n> index bda10dd5c85ffd8988a6c3d39583e7b9701278b8..65d2de2c2051cf28def8b43fcca3fd5e1c86a0d8 100644\n> --- a/remote.h\n> +++ b/remote.h\n> @@ -219,6 +219,7 @@ struct ref *alloc_ref(const char *name);\n>  struct ref *copy_ref(const struct ref *ref);\n>  struct ref *copy_ref_list(const struct ref *ref);\n>  int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n> +void tail_link_ref(struct ref *ref, struct ref ***tail);\n\nCan we maybe add a comment explaining what this function does?\n\nPatrick\n"},{"id":"511694","messageId":"Z6B1bM7bbpNUNjpt@pks.im","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-6-2a4ff851498f@iotcl.com","subject":"Re: [PATCH v4 6/6] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-03T07:51:37Z","receivedAt":"2025-02-03T07:51:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 31, 2025 at 04:30:34PM +0100, Toon Claes wrote:\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index f92017c751dd31cb25a3ba31667b015d5766ce84..40d6ecfa74608193a88715a29b4ca765687a0c86 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -585,6 +586,10 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>  \t} else if (our) {\n>  \t\tstruct commit *c = lookup_commit_reference(the_repository,\n>  \t\t\t\t\t\t\t   &our->old_oid);\n> +\n> +\t\tif (!c)\n> +\t\t\tdie(_(\"couldn't look up commit object for '%s'\"), our->name);\n\nYou can use `lookup_commit_or_die()` instead.\n\n> @@ -1500,6 +1534,12 @@ int cmd_clone(int argc,\n>  \t\tif (!our_head_points_at)\n>  \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n>  \t\t\t    option_branch, remote_name);\n> +\t} else if (option_rev) {\n> +\t\tour_head_points_at = mapped_refs;\n> +\t\tif (!our_head_points_at)\n> +\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n> +\t\t\t    option_rev, remote_name);\n> +\t\t//mapped_refs->name[0] = 0;\n\nThis looks like a left-over debug statement?\n\n> diff --git a/parse-options.h b/parse-options.h\n> index 39f088625494f20dea96b9a9cbe986916773bf60..fca944d9a93d643d984c58de2ead9154c8b16c94 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -436,6 +436,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n>  \t\t\t\t  0, \"\");\n>  }\n>  \n> +static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n> +\t\t\t\t\t     int opt2, const char *opt2_name)\n> +{\n> +\tdie_for_incompatible_opt4(opt1, opt1_name,\n> +\t\t\t\t  opt2, opt2_name,\n> +\t\t\t\t  0, \"\",\n> +\t\t\t\t  0, \"\");\n> +}\n> +\n>  /*\n>   * Use these assertions for callbacks that expect to be called with NONEG and\n>   * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n\nIt might make sense to introduce this in a separate commit, ideally with\nan example callsite. `grep die\\(.*incompatible` surfaces a couple of\ncandidates.\n\nPatrick\n"},{"id":"511827","messageId":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com","subject":"[PATCH v5 0/7] Enable doing a shallow clone of a specific git revision","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:33:59Z","receivedAt":"2025-02-04T21:34:21Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The goal of this series is to add an option `--revision` to\ngit-clone(1).\n\nThis series starts with a handful of preparatory refactoring commits\nthat make it more straight-forward to add this new option. In the last\ncommit we're actually adding the feature.\n\nThis series sets an example on how I think we can further refactor\nbuiltin/clone.c to increase the maintainability of the code.\n\n---\nChanges in v5:\n- Add separate commit to introduce die_for_incompatible_opt2()\n- Small tweaks in documentation about `--[no-]tags` and `--revision`.\n- Better explain the refactoring of wanted_peer_refs() in the commit\n  message.\n- Change type from `int` to `size_t` in wanted_peer_refs().\n- Use lookup_commit_or_die() instead lookup_commit_reference() to avoid\n  checking the result ourself.\n- Add a few code comments to explain some things.\n- Stylish cleanups like removal of unneeded empty lines, commented out\n  test-code and remarks.\n- Link to v4: https://lore.kernel.org/r/20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com\n\nChanges in v4:\n- Introduce a new commit to reduce the use of global variables.\n- Introduce a new commit to invert the flag --no-tags to --tags.\n- Introduce a new commit to refactor wanted_peer_refs() in\n  builtin/clone.c.\n- Introduce a new commit to shuffle the handling of tags refspec.\n- Introduce a new commit to introduce a `struct clone_opts`.\n- Link to v3: https://lore.kernel.org/r/20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com\n\nChanges in v3:\n- Fail early when the revision was not found on the remote, instead of\n  creating a clone that's in an invalid state.\n- State more clearly in the commit message adding this option is useful\n  for a not uncommon use-case.\n- Be explicit in the documentation the ref needs to peel down to a\n  commit.\n- Die in case we try to update_head() to an object that's not a commit.\n- Allow combining `--revision` with `--bare`.\n- Add die_for_incompatible_opt2() to parse-options.h and use it for the\n  options that are not compatible with the new `--revision` option.\n- Small tweaks to the added tests.\n- Small touchups on commit messages.\n- Link to v2: https://lore.kernel.org/r/20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com\n\n---\nToon Claes (7):\n      clone: cut down on global variables in clone.c\n      clone: make it possible to specify --tags\n      clone: refactor wanted_peer_refs()\n      clone: add tags refspec earlier to fetch refspec\n      clone: introduce struct clone_opts in builtin/clone.c\n      parse-options: introduce die_for_incompatible_opt2()\n      builtin/clone: teach git-clone(1) the --revision= option\n\n Documentation/git-clone.txt |  17 ++-\n builtin/clone.c             | 350 +++++++++++++++++++++++++-------------------\n builtin/replay.c            |   3 +-\n parse-options.h             |   9 ++\n remote.c                    |   2 +-\n remote.h                    |   5 +\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++\n 8 files changed, 351 insertions(+), 159 deletions(-)\n---\n\nRange-diff versus v4:\n\n1:  a563ae1023 = 1:  43fbfe2a1c clone: cut down on global variables in clone.c\n2:  031fec1961 ! 2:  68bbf04606 clone: make it possible to specify --tags\n    @@ Documentation/git-clone.txt: git clone [--template=<template-directory>]\n      \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]\n      \t  [--dissociate] [--separate-git-dir <git-dir>]\n     -\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n    -+\t  [--depth <depth>] [--[no-]single-branch] [--[no-]-tags]\n    ++\t  [--depth <depth>] [--[no-]single-branch] [--[no-]tags]\n      \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n      \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]\n      \t  [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository>\n    @@ Documentation/git-clone.txt: corresponding `--mirror` and `--no-tags` options in\n     -`--no-tags`::\n     -\tDon't clone any tags, and set\n     +`--[no-]tags`::\n    -+\tBy default tags are cloned, and passing `--tags` doesn't change that.\n     +\tWith `--no-tags`, no tags are cloned, and set\n      \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n      \tthat future `git pull` and `git fetch` operations won't follow\n      \tany tags. Subsequent explicit tag fetches will still work,\n    + \t(see linkgit:git-fetch[1]).\n    ++\tBy default tags are cloned, and passing `--tags` doesn't change that.\n    + +\n    + Can be used in conjunction with `--single-branch` to clone and\n    + maintain a branch with no references other than a single cloned\n     \n      ## builtin/clone.c ##\n     @@\n3:  b926bcc1df ! 3:  ac0babfc2a clone: refactor wanted_peer_refs()\n    @@ Commit message\n     \n         Over time this function grown to be very complex. Refactor it.\n     \n    +    Previously, there was a separate code path for when\n    +    `option_single_branch` was set. It resulted in duplicated code and\n    +    deeper nested conditions. After this refactor the code path for when\n    +    `option_single_branch` is truthy modifies `refs` and then falls through\n    +    to the common code path. This approach relies on the `refspec` being set\n    +    correctly and thus only mapping refs that are relevant.\n    +\n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n      ## builtin/clone.c ##\n    @@ builtin/clone.c: static struct ref *wanted_peer_refs(const struct ref *refs,\n      \t}\n      \n     -\tif (!option_mirror && !option_single_branch && option_tags)\n    -+\tfor (int i = 0; i < refspec->nr; i++)\n    ++\tfor (size_t i = 0; i < refspec->nr; i++)\n     +\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n     +\n     +\t/*\n4:  2201b996b3 ! 4:  8a98961fd5 clone: add tags refspec earlier to fetch refspec\n    @@ Commit message\n     \n         In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\n         from the `remote->fetch` refspec into `ref_prefixes` of\n    -    `transport_ls_refs_options`. Afterward we add the tags prefix\n    +    `transport_ls_refs_options`. Afterwards we add the tags prefix\n         `refs/tags/` prefix as well. At a later point, in wanted_peer_refs() we\n         process refs using both `remote->fetch` and `TAG_REFSPEC`.\n     \n    @@ builtin/clone.c: static struct ref *wanted_peer_refs(const struct ref *refs,\n      \t\tif (!option_branch)\n      \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n     @@ builtin/clone.c: static struct ref *wanted_peer_refs(const struct ref *refs,\n    - \tfor (int i = 0; i < refspec->nr; i++)\n    + \tfor (size_t i = 0; i < refspec->nr; i++)\n      \t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n      \n     -\t/*\n    @@ builtin/clone.c: int cmd_clone(int argc,\n      \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n     +\n     +\tif (option_tags || option_branch)\n    ++\t\t/*\n    ++\t\t * Add tags refspec when user asked for tags (implicitly) or\n    ++\t\t * specified --branch, which argument might be a tag.\n    ++\t\t */\n     +\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n     +\n      \trefspec_ref_prefixes(&remote->fetch,\n5:  14fb827c41 ! 5:  b338eec186 clone: introduce struct clone_opts in builtin/clone.c\n    @@ builtin/clone.c: static struct ref *find_remote_branch(const struct ref *refs, c\n     +\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n     +\t\tif (head)\n     +\t\t\ttail_link_ref(head, &tail);\n    -+\n     +\t\tif (option_single_branch)\n      \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n     -\t\telse {\n    @@ builtin/clone.c: static struct ref *find_remote_branch(const struct ref *refs, c\n     -\t\t\ttail = &local_refs;\n     -\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n     -\t\t}\n    -+\t}\n    -+\n    -+\telse if (option_single_branch) {\n    ++\t} else if (option_single_branch) {\n     +\t\tlocal_refs = NULL;\n     +\t\ttail = &local_refs;\n     +\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n      \t}\n      \n    - \tfor (int i = 0; i < refspec->nr; i++)\n    + \tfor (size_t i = 0; i < refspec->nr; i++)\n     @@ builtin/clone.c: int cmd_clone(int argc,\n      \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n      \tconst char *bundle_uri = NULL;\n    @@ remote.h: struct ref *alloc_ref(const char *name);\n      struct ref *copy_ref(const struct ref *ref);\n      struct ref *copy_ref_list(const struct ref *ref);\n      int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n    ++/*\n    ++ * Put a ref in the tail and prepare tail for adding another one.\n    ++ * *tail is the pointer to the tail of the list of refs.\n    ++ */\n     +void tail_link_ref(struct ref *ref, struct ref ***tail);\n      \n      int check_ref_type(const struct ref *ref, int flags);\n-:  ---------- > 6:  d312865d63 parse-options: introduce die_for_incompatible_opt2()\n6:  d87d155dfc ! 7:  8ddbc6eb41 builtin/clone: teach git-clone(1) the --revision= option\n    @@ Documentation/git-clone.txt: objects from the source repository into a pack in t\n      \t`--branch` can also take tags and detaches the `HEAD` at that commit\n      \tin the resulting repository.\n      \n    -+`--revision` _<rev>_::\n    ++`--revision=<rev>`::\n     +\tCreate a new repository, and fetch the history leading to the given\n     +\trevision _<rev>_ (and nothing else), without making any remote-tracking\n     +\tbranch, and without making any local branch, and point `HEAD` to\n    @@ builtin/clone.c: static void update_remote_refs(const struct ref *refs,\n      \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n      \t\t\tdie(_(\"unable to update HEAD\"));\n     @@ builtin/clone.c: static void update_head(const struct ref *our, const struct ref *remote,\n    + \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n    + \t\t}\n      \t} else if (our) {\n    - \t\tstruct commit *c = lookup_commit_reference(the_repository,\n    - \t\t\t\t\t\t\t   &our->old_oid);\n    -+\n    -+\t\tif (!c)\n    -+\t\t\tdie(_(\"couldn't look up commit object for '%s'\"), our->name);\n    +-\t\tstruct commit *c = lookup_commit_reference(the_repository,\n    +-\t\t\t\t\t\t\t   &our->old_oid);\n    ++\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n    ++\t\t\t\t\t\t        our->name);\n     +\n      \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n      \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\t\t\t\t  !!option_branch, \"--branch\");\n     +\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n     +\t\t\t\t  option_mirror, \"--mirror\");\n    -+\t// TODO --no-single-branch\n     +\n      \tif (reject_shallow)\n      \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     -\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n     +\tif (option_rev) {\n     +\t\toption_tags = 0;\n    -+\t\toption_branch = 0;\n     +\t\toption_single_branch = 0;\n     +\t\topts.wants_head = 0;\n     +\t\topts.detach = 1;\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\t}\n      \n      \tif (option_tags || option_branch)\n    - \t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n    + \t\t/*\n     @@ builtin/clone.c: int cmd_clone(int argc,\n      \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n      \t\t\t\t  option_branch);\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\t\tif (!our_head_points_at)\n     +\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n     +\t\t\t    option_rev, remote_name);\n    -+\t\t//mapped_refs->name[0] = 0;\n      \t} else if (remote_head_points_at) {\n      \t\tour_head_points_at = remote_head_points_at;\n      \t} else if (remote_head) {\n    @@ builtin/clone.c: int cmd_clone(int argc,\n      \t/*\n      \t * We want to show progress for recursive submodule clones iff\n     \n    - ## parse-options.h ##\n    -@@ parse-options.h: static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n    - \t\t\t\t  0, \"\");\n    - }\n    - \n    -+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n    -+\t\t\t\t\t     int opt2, const char *opt2_name)\n    -+{\n    -+\tdie_for_incompatible_opt4(opt1, opt1_name,\n    -+\t\t\t\t  opt2, opt2_name,\n    -+\t\t\t\t  0, \"\",\n    -+\t\t\t\t  0, \"\");\n    -+}\n    -+\n    - /*\n    -  * Use these assertions for callbacks that expect to be called with NONEG and\n    -  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n    -\n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n        't5617-clone-submodules-remote.sh',\n\n\n---\n\nbase-commit: bc204b742735ae06f65bb20291c95985c9633b7f\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nThanks\n--\nToon\n\n"},{"id":"511829","messageId":"20250204-toon-clone-refs-v5-1-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 1/7] clone: cut down on global variables in clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:00Z","receivedAt":"2025-02-04T21:34:26Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c the `struct option` which is used to parse the input options\nfor git-clone(1) is a global variable. Due to this, many variables that\nare used to parse the value into, are also global.\n\nMake `builtin_clone_options` a local variable in cmd_clone() and carry\nalong all variables that are only used in that function.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 195 +++++++++++++++++++++++++++++---------------------------\n 1 file changed, 101 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd001d800c635e46bbc7027a8fdda2a8c9fbf069..5ed0802f1d0ddebaf512aac93bf8c8b340494323 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -56,42 +56,22 @@\n  *  - dropping use-separate-remote and no-separate-remote compatibility\n  *\n  */\n-static const char * const builtin_clone_usage[] = {\n-\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n-\tNULL\n-};\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_no_tags;\n static int option_shallow_submodules;\n-static int option_reject_shallow = -1;    /* unspecified */\n static int config_reject_shallow = -1;    /* unspecified */\n-static int deepen;\n-static char *option_template, *option_depth, *option_since;\n-static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n-static struct string_list option_not = STRING_LIST_INIT_NODUP;\n-static const char *real_git_dir;\n-static const char *ref_format;\n-static const char *option_upload_pack = \"git-upload-pack\";\n static int option_verbosity;\n-static int option_progress = -1;\n-static int option_sparse_checkout;\n-static enum transport_family family;\n-static struct string_list option_config = STRING_LIST_INIT_NODUP;\n static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n-static int option_dissociate;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n-static int option_filter_submodules = -1;    /* unspecified */\n static int config_filter_submodules = -1;    /* unspecified */\n-static struct string_list server_options = STRING_LIST_INIT_NODUP;\n static int option_remote_submodules;\n-static const char *bundle_uri;\n \n static int recurse_submodules_cb(const struct option *opt,\n \t\t\t\t const char *arg, int unset)\n@@ -107,78 +87,6 @@ static int recurse_submodules_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static struct option builtin_clone_options[] = {\n-\tOPT__VERBOSITY(&option_verbosity),\n-\tOPT_BOOL(0, \"progress\", &option_progress,\n-\t\t N_(\"force progress reporting\")),\n-\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n-\t\t N_(\"don't clone shallow repository\")),\n-\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n-\t\t N_(\"don't create a checkout\")),\n-\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n-\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n-\t\t\tN_(\"create a bare repository\")),\n-\tOPT_BOOL(0, \"mirror\", &option_mirror,\n-\t\t N_(\"create a mirror repository (implies --bare)\")),\n-\tOPT_BOOL('l', \"local\", &option_local,\n-\t\tN_(\"to clone from a local repository\")),\n-\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n-\t\t    N_(\"don't use local hardlinks, always copy\")),\n-\tOPT_BOOL('s', \"shared\", &option_shared,\n-\t\t    N_(\"setup as shared repository\")),\n-\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n-\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules cloned in parallel\")),\n-\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n-\t\t   N_(\"directory from which templates will be used\")),\n-\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n-\t\t\tN_(\"reference repository\")),\n-\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n-\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n-\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n-\t\t N_(\"use --reference only while cloning\")),\n-\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n-\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n-\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n-\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n-\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to git-upload-pack on the remote\")),\n-\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n-\t\t    N_(\"create a shallow clone of that depth\")),\n-\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n-\t\t    N_(\"create a shallow clone since a specific time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n-\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n-\t\t    N_(\"clone only one branch, HEAD or --branch\")),\n-\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n-\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n-\t\t    N_(\"any cloned submodules will be shallow\")),\n-\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n-\t\t   N_(\"separate git dir from working tree\")),\n-\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n-\t\t   N_(\"specify the reference format to use\")),\n-\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n-\t\t\tN_(\"set config inside the new repository\")),\n-\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n-\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_IPVERSION(&family),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n-\t\t    N_(\"apply partial clone filters to submodules\")),\n-\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n-\t\t    N_(\"any cloned submodules will use their remote-tracking branch\")),\n-\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n-\t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n-\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n-\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n-\tOPT_END()\n-};\n-\n static const char *get_repo_path_1(struct strbuf *path, int *is_bundle)\n {\n \tstatic const char *suffix[] = { \"/.git\", \"\", \".git/.git\", \".git\" };\n@@ -989,10 +897,103 @@ int cmd_clone(int argc,\n \tint hash_algo;\n \tenum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n \tconst int do_not_override_repo_unix_permissions = -1;\n+\tint option_reject_shallow = -1; /* unspecified */\n+\tint deepen = 0;\n+\tchar *option_template = NULL, *option_depth = NULL, *option_since = NULL;\n+\tchar *option_origin = NULL;\n+\tstruct string_list option_not = STRING_LIST_INIT_NODUP;\n+\tconst char *real_git_dir = NULL;\n+\tconst char *ref_format = NULL;\n+\tconst char *option_upload_pack = \"git-upload-pack\";\n+\tint option_progress = -1;\n+\tint option_sparse_checkout = 0;\n+\tenum transport_family family = TRANSPORT_FAMILY_ALL;\n+\tstruct string_list option_config = STRING_LIST_INIT_DUP;\n+\tint option_dissociate = 0;\n+\tint option_filter_submodules = -1; /* unspecified */\n+\tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n+\tconst char *bundle_uri = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n+\tstruct option builtin_clone_options[] = {\n+\t\tOPT__VERBOSITY(&option_verbosity),\n+\t\tOPT_BOOL(0, \"progress\", &option_progress,\n+\t\t\t N_(\"force progress reporting\")),\n+\t\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n+\t\t\t N_(\"don't clone shallow repository\")),\n+\t\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n+\t\t\t N_(\"don't create a checkout\")),\n+\t\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n+\t\t\t\tN_(\"create a bare repository\")),\n+\t\tOPT_BOOL(0, \"mirror\", &option_mirror,\n+\t\t\t N_(\"create a mirror repository (implies --bare)\")),\n+\t\tOPT_BOOL('l', \"local\", &option_local,\n+\t\t\t N_(\"to clone from a local repository\")),\n+\t\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n+\t\t\t N_(\"don't use local hardlinks, always copy\")),\n+\t\tOPT_BOOL('s', \"shared\", &option_shared,\n+\t\t\t N_(\"setup as shared repository\")),\n+\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n+\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n+\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules cloned in parallel\")),\n+\t\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n+\t\t\t   N_(\"directory from which templates will be used\")),\n+\t\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n+\t\t\t\tN_(\"reference repository\")),\n+\t\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n+\t\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n+\t\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n+\t\t\t N_(\"use --reference only while cloning\")),\n+\t\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n+\t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n+\t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to git-upload-pack on the remote\")),\n+\t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n+\t\t\t   N_(\"create a shallow clone of that depth\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n+\t\t\t   N_(\"create a shallow clone since a specific time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n+\t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n+\t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n+\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n+\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n+\t\t\t N_(\"any cloned submodules will be shallow\")),\n+\t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n+\t\t\t   N_(\"separate git dir from working tree\")),\n+\t\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n+\t\t\t   N_(\"specify the reference format to use\")),\n+\t\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n+\t\t\t\tN_(\"set config inside the new repository\")),\n+\t\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_IPVERSION(&family),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n+\t\t\t N_(\"apply partial clone filters to submodules\")),\n+\t\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n+\t\t\t N_(\"any cloned submodules will use their remote-tracking branch\")),\n+\t\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n+\t\t\t N_(\"initialize sparse-checkout file to include only files at root\")),\n+\t\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n+\t\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tconst char * const builtin_clone_usage[] = {\n+\t\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n+\t\tNULL\n+\t};\n+\n \tpacket_trace_identity(\"clone\");\n \n \tgit_config(git_clone_config, NULL);\n@@ -1138,8 +1139,8 @@ int cmd_clone(int argc,\n \t\tfor_each_string_list_item(item, &option_recurse_submodules) {\n \t\t\tstrbuf_addf(&sb, \"submodule.active=%s\",\n \t\t\t\t    item->string);\n-\t\t\tstring_list_append(&option_config,\n-\t\t\t\t\t   strbuf_detach(&sb, NULL));\n+\t\t\tstring_list_append(&option_config, sb.buf);\n+\t\t\tstrbuf_reset(&sb);\n \t\t}\n \n \t\tif (!git_config_get_bool(\"submodule.stickyRecursiveClone\", &val) &&\n@@ -1161,6 +1162,8 @@ int cmd_clone(int argc,\n \t\t\tstring_list_append(&option_config,\n \t\t\t\t\"submodule.alternateErrorStrategy=info\");\n \t\t}\n+\n+\t\tstrbuf_release(&sb);\n \t}\n \n \t/*\n@@ -1578,6 +1581,10 @@ int cmd_clone(int argc,\n \terr = checkout(submodule_progress, filter_submodules,\n \t\t       ref_storage_format);\n \n+\tstring_list_clear(&option_not, 0);\n+\tstring_list_clear(&option_config, 0);\n+\tstring_list_clear(&server_options, 0);\n+\n \tfree(remote_name);\n \tstrbuf_release(&reflog_msg);\n \tstrbuf_release(&branch_top);\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511828","messageId":"20250204-toon-clone-refs-v5-2-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 2/7] clone: make it possible to specify --tags","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:01Z","receivedAt":"2025-02-04T21:34:29Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --no-tags was added in 0dab2468ee (clone: add a --no-tags option\nto clone without tags, 2017-04-26). At the time there was no need to\nsupport --tags as well, although there was some conversation about\nit[1].\n\nTo simplify the code and to prepare for future commits, invert the flag\ninternally. Functionally there is no change, because the flag is\ndefault-enabled passing `--tags` has no effect, so there's no need to\nadd tests for this.\n\n[1]: https://lore.kernel.org/git/CAGZ79kbHuMpiavJ90kQLEL_AR0BEyArcZoEWAjPPhOFacN16YQ@mail.gmail.com/\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |  7 ++++---\n builtin/clone.c             | 14 +++++++-------\n 2 files changed, 11 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex de8d8f58930ecff305f79480b13ddce10cd96c60..e89ae2e8c664f4d4e15e5f05264c8df988295e3d 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -13,7 +13,7 @@ git clone [--template=<template-directory>]\n \t  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]\n \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]\n \t  [--dissociate] [--separate-git-dir <git-dir>]\n-\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n+\t  [--depth <depth>] [--[no-]single-branch] [--[no-]tags]\n \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]\n \t  [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository>\n@@ -273,12 +273,13 @@ corresponding `--mirror` and `--no-tags` options instead.\n \tbranch when `--single-branch` clone was made, no remote-tracking\n \tbranch is created.\n \n-`--no-tags`::\n-\tDon't clone any tags, and set\n+`--[no-]tags`::\n+\tWith `--no-tags`, no tags are cloned, and set\n \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n \tthat future `git pull` and `git fetch` operations won't follow\n \tany tags. Subsequent explicit tag fetches will still work,\n \t(see linkgit:git-fetch[1]).\n+\tBy default tags are cloned, and passing `--tags` doesn't change that.\n +\n Can be used in conjunction with `--single-branch` to clone and\n maintain a branch with no references other than a single cloned\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5ed0802f1d0ddebaf512aac93bf8c8b340494323..69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,7 +59,7 @@\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n-static int option_no_tags;\n+static int option_tags = 1; /* default enabled */\n static int option_shallow_submodules;\n static int config_reject_shallow = -1;    /* unspecified */\n static char *remote_name = NULL;\n@@ -470,7 +470,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && !option_no_tags)\n+\tif (!option_mirror && !option_single_branch && option_tags)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n \trefspec_item_clear(&tag_refspec);\n@@ -562,7 +562,7 @@ static void update_remote_refs(const struct ref *refs,\n \n \tif (refs) {\n \t\twrite_remote_refs(mapped_refs);\n-\t\tif (option_single_branch && !option_no_tags)\n+\t\tif (option_single_branch && option_tags)\n \t\t\twrite_followtags(refs, msg);\n \t}\n \n@@ -964,8 +964,8 @@ int cmd_clone(int argc,\n \t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n \t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n \t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n-\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"tags\", &option_tags,\n+\t\t\t N_(\"clone tags, and make later fetches not to follow them\")),\n \t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n \t\t\t N_(\"any cloned submodules will be shallow\")),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n@@ -1296,7 +1296,7 @@ int cmd_clone(int argc,\n \tgit_config_set(key.buf, repo);\n \tstrbuf_reset(&key);\n \n-\tif (option_no_tags) {\n+\tif (!option_tags) {\n \t\tstrbuf_addf(&key, \"remote.%s.tagOpt\", remote_name);\n \t\tgit_config_set(key.buf, \"--no-tags\");\n \t\tstrbuf_reset(&key);\n@@ -1389,7 +1389,7 @@ int cmd_clone(int argc,\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (!option_no_tags)\n+\tif (option_tags)\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511830","messageId":"20250204-toon-clone-refs-v5-3-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 3/7] clone: refactor wanted_peer_refs()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:02Z","receivedAt":"2025-02-04T21:34:31Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The function wanted_peer_refs() is used to map the refs returned by the\nserver to refs we will save in our clone.\n\nOver time this function grown to be very complex. Refactor it.\n\nPreviously, there was a separate code path for when\n`option_single_branch` was set. It resulted in duplicated code and\ndeeper nested conditions. After this refactor the code path for when\n`option_single_branch` is truthy modifies `refs` and then falls through\nto the common code path. This approach relies on the `refspec` being set\ncorrectly and thus only mapping refs that are relevant.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 39 +++++++++++++++------------------------\n 1 file changed, 15 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4..5efa2bbceb42b230f723660ea963ca1253888235 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -434,46 +434,37 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n {\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n-\tstruct ref **tail = head ? &head->next : &local_refs;\n+\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n \tstruct refspec_item tag_refspec;\n+\tstruct ref *to_free = NULL;\n \n \trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n \n \tif (option_single_branch) {\n-\t\tstruct ref *remote_head = NULL;\n-\n \t\tif (!option_branch)\n-\t\t\tremote_head = guess_remote_head(head, refs, 0);\n+\t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n \t\telse {\n \t\t\tfree_one_ref(head);\n \t\t\tlocal_refs = head = NULL;\n \t\t\ttail = &local_refs;\n-\t\t\tremote_head = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n-\n-\t\tif (!remote_head && option_branch)\n-\t\t\twarning(_(\"Could not find remote branch %s to clone.\"),\n-\t\t\t\toption_branch);\n-\t\telse {\n-\t\t\tint i;\n-\t\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\t\tget_fetch_map(remote_head, &refspec->items[i],\n-\t\t\t\t\t      &tail, 0);\n-\n-\t\t\t/* if --branch=tag, pull the requested tag explicitly */\n-\t\t\tget_fetch_map(remote_head, &tag_refspec, &tail, 0);\n+\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t\t}\n-\t\tfree_refs(remote_head);\n-\t} else {\n-\t\tint i;\n-\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && option_tags)\n+\tfor (size_t i = 0; i < refspec->nr; i++)\n+\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n+\n+\t/*\n+\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n+\t * about won't be present in `refs` anyway.\n+\t * Except with option --mirror, where we grab all refs already.\n+\t */\n+\tif (!option_mirror)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n+\tfree_one_ref(to_free);\n \trefspec_item_clear(&tag_refspec);\n+\n \treturn local_refs;\n }\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511831","messageId":"20250204-toon-clone-refs-v5-4-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 4/7] clone: add tags refspec earlier to fetch refspec","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:03Z","receivedAt":"2025-02-04T21:34:38Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\nfrom the `remote->fetch` refspec into `ref_prefixes` of\n`transport_ls_refs_options`. Afterwards we add the tags prefix\n`refs/tags/` prefix as well. At a later point, in wanted_peer_refs() we\nprocess refs using both `remote->fetch` and `TAG_REFSPEC`.\n\nSimplify the code by appending `TAG_REFSPEC` to `remote->fetch` before\ncalling refspec_ref_prefixes().\n\nTo be able to do this, we set `option_tags` to 0 when --mirror is given.\nThis is because --mirror mirrors (hence the name) all the refs,\nincluding tags and they do not need to be treated separately.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5efa2bbceb42b230f723660ea963ca1253888235..1419b82a7bdd3d91cc08667a854b84ba33d1e7aa 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -435,11 +435,8 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n \tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n-\tstruct refspec_item tag_refspec;\n \tstruct ref *to_free = NULL;\n \n-\trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n-\n \tif (option_single_branch) {\n \t\tif (!option_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n@@ -454,16 +451,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tfor (size_t i = 0; i < refspec->nr; i++)\n \t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \n-\t/*\n-\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n-\t * about won't be present in `refs` anyway.\n-\t * Except with option --mirror, where we grab all refs already.\n-\t */\n-\tif (!option_mirror)\n-\t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n-\n \tfree_one_ref(to_free);\n-\trefspec_item_clear(&tag_refspec);\n \n \treturn local_refs;\n }\n@@ -1011,8 +999,10 @@ int cmd_clone(int argc,\n \t\t\tdie(_(\"unknown ref storage format '%s'\"), ref_format);\n \t}\n \n-\tif (option_mirror)\n+\tif (option_mirror) {\n \t\toption_bare = 1;\n+\t\toption_tags = 0;\n+\t}\n \n \tif (option_bare) {\n \t\tif (real_git_dir)\n@@ -1375,14 +1365,19 @@ int cmd_clone(int argc,\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n+\tif (option_tags || option_branch)\n+\t\t/*\n+\t\t * Add tags refspec when user asked for tags (implicitly) or\n+\t\t * specified --branch, which argument might be a tag.\n+\t\t */\n+\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n+\n \trefspec_ref_prefixes(&remote->fetch,\n \t\t\t     &transport_ls_refs_options.ref_prefixes);\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (option_tags)\n-\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n-\t\t\t    \"refs/tags/\");\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511832","messageId":"20250204-toon-clone-refs-v5-5-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 5/7] clone: introduce struct clone_opts in builtin/clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:04Z","receivedAt":"2025-02-04T21:34:47Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"There is a lot of state stored in global variables in builtin/clone.c.\nIn the long run we'd like to remove many of those.\n\nIntroduce `struct clone_opts` in this file. This struct will be used to\ncontain all details needed to perform the clone. The struct object can\nbe thrown around to all the functions that need these details.\n\nThe first field we're adding is `wants_head`. In some scenarios\n(specifically when both `--single-branch` and `--branch` are given) we\nare not interested in `HEAD` on the remote. The field `wants_head` in\n`struct clone_opts` will hold this information. We could have put\n`option_branch` and `option_single_branch` into that struct instead, but\nin a following commit we'll be using `wants_head` as well.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 44 +++++++++++++++++++++++++++++---------------\n remote.c        |  2 +-\n remote.h        |  5 +++++\n 3 files changed, 35 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 1419b82a7bdd3d91cc08667a854b84ba33d1e7aa..275940f4f5037de65d0f33eba3e7bc031f9b122b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -57,6 +57,13 @@\n  *\n  */\n \n+struct clone_opts {\n+\tint wants_head;\n+};\n+#define CLONE_OPTS_INIT { \\\n+\t.wants_head = 1 /* default enabled */ \\\n+}\n+\n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_tags = 1; /* default enabled */\n@@ -429,23 +436,24 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch\n \treturn ref;\n }\n \n-static struct ref *wanted_peer_refs(const struct ref *refs,\n-\t\tstruct refspec *refspec)\n+static struct ref *wanted_peer_refs(struct clone_opts *opts,\n+\t\t\t\t    const struct ref *refs,\n+\t\t\t\t    struct refspec *refspec)\n {\n-\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n-\tstruct ref *local_refs = head;\n-\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n+\tstruct ref *local_refs = NULL;\n+\tstruct ref **tail = &local_refs;\n \tstruct ref *to_free = NULL;\n \n-\tif (option_single_branch) {\n-\t\tif (!option_branch)\n+\tif (opts->wants_head) {\n+\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n+\t\tif (head)\n+\t\t\ttail_link_ref(head, &tail);\n+\t\tif (option_single_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n-\t\telse {\n-\t\t\tfree_one_ref(head);\n-\t\t\tlocal_refs = head = NULL;\n-\t\t\ttail = &local_refs;\n-\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n+\t} else if (option_single_branch) {\n+\t\tlocal_refs = NULL;\n+\t\ttail = &local_refs;\n+\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t}\n \n \tfor (size_t i = 0; i < refspec->nr; i++)\n@@ -893,6 +901,8 @@ int cmd_clone(int argc,\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n \n+\tstruct clone_opts opts = CLONE_OPTS_INIT;\n+\n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n@@ -1343,9 +1353,13 @@ int cmd_clone(int argc,\n \tif (option_not.nr)\n \t\ttransport_set_option(transport, TRANS_OPT_DEEPEN_NOT,\n \t\t\t\t     (const char *)&option_not);\n-\tif (option_single_branch)\n+\tif (option_single_branch) {\n \t\ttransport_set_option(transport, TRANS_OPT_FOLLOWTAGS, \"1\");\n \n+\t\tif (option_branch)\n+\t\t\topts.wants_head = 0;\n+\t}\n+\n \tif (option_upload_pack)\n \t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK,\n \t\t\t\t     option_upload_pack);\n@@ -1454,7 +1468,7 @@ int cmd_clone(int argc,\n \t}\n \n \tif (refs)\n-\t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t\tmapped_refs = wanted_peer_refs(&opts, refs, &remote->fetch);\n \n \tif (mapped_refs) {\n \t\t/*\ndiff --git a/remote.c b/remote.c\nindex 1779f0e7bbb8b88965f2fedf407e50ed20ea7a13..69d8c43ea694f1b9a7699e5d5a49bfc169058b8e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1260,7 +1260,7 @@ int count_refspec_match(const char *pattern,\n \t}\n }\n \n-static void tail_link_ref(struct ref *ref, struct ref ***tail)\n+void tail_link_ref(struct ref *ref, struct ref ***tail)\n {\n \t**tail = ref;\n \twhile (ref->next)\ndiff --git a/remote.h b/remote.h\nindex a19353f68999f5440db7bf5f91dd4be8bcc1d8a5..ce3e7c8512981d0ac5db2da508c4fbe64cede961 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -221,6 +221,11 @@ struct ref *alloc_ref(const char *name);\n struct ref *copy_ref(const struct ref *ref);\n struct ref *copy_ref_list(const struct ref *ref);\n int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n+/*\n+ * Put a ref in the tail and prepare tail for adding another one.\n+ * *tail is the pointer to the tail of the list of refs.\n+ */\n+void tail_link_ref(struct ref *ref, struct ref ***tail);\n \n int check_ref_type(const struct ref *ref, int flags);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511833","messageId":"20250204-toon-clone-refs-v5-6-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 6/7] parse-options: introduce die_for_incompatible_opt2()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:05Z","receivedAt":"2025-02-04T21:34:50Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The functions die_for_incompatible_opt3() and\ndie_for_incompatible_opt4() already exist to die whenever a user\nspecifies three or four options respectively that are not compatible.\n\nIntroduce die_for_incompatible_opt2() which dies when two options that\nare incompatible are set.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/replay.c | 3 ++-\n parse-options.h  | 9 +++++++++\n 2 files changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1afc6d1ee0cb738fa7fa3f2b5c8ce0dd7802e7da..03d93afd77290ab556565f05bd424956b66ff01c 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -164,7 +164,8 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \tif (onto_name && *advance_name)\n-\t\tdie(_(\"--onto and --advance are incompatible\"));\n+\t\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n+\t\t\t\t\t  !!*advance_name, \"--advance\");\n \telse if (onto_name) {\n \t\t*onto = peel_committish(onto_name);\n \t\tif (rinfo.positive_refexprs <\ndiff --git a/parse-options.h b/parse-options.h\nindex 39f088625494f20dea96b9a9cbe986916773bf60..fca944d9a93d643d984c58de2ead9154c8b16c94 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -436,6 +436,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n \t\t\t\t  0, \"\");\n }\n \n+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n+\t\t\t\t\t     int opt2, const char *opt2_name)\n+{\n+\tdie_for_incompatible_opt4(opt1, opt1_name,\n+\t\t\t\t  opt2, opt2_name,\n+\t\t\t\t  0, \"\",\n+\t\t\t\t  0, \"\");\n+}\n+\n /*\n  * Use these assertions for callbacks that expect to be called with NONEG and\n  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511834","messageId":"20250204-toon-clone-refs-v5-7-37e34af283c8@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v5 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-04T21:34:06Z","receivedAt":"2025-02-04T21:34:57Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot be selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a hexadecimal commit ID. This enables the user\nto clone and check out any revision they want. `--revision` can be used\nin conjunction with `--depth` to do a minimal clone that only contains\nthe blob and tree for a single revision. This can be useful for\nautomated tests running in CI systems.\n\nUsing option `--branch` and `--single-branch` together is a similar\nscenario, but serves a different purpose. Using these two options, a\nsinglet remote tracking branch is created and the fetch refspec is set\nup so git-fetch(1) will receive updates on that branch from the remote.\nThis allows the user work on that single branch.\n\nOption `--revision` on contrary detaches HEAD, creates no tracking\nbranches, and writes no fetch refspec.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |  10 ++++\n builtin/clone.c             |  57 ++++++++++++++++----\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 180 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex e89ae2e8c664f4d4e15e5f05264c8df988295e3d..7bf6adb98350a9202b01a58f86ee210d9cb85abc 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -221,6 +221,16 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision=<rev>`::\n+\tCreate a new repository, and fetch the history leading to the given\n+\trevision _<rev>_ (and nothing else), without making any remote-tracking\n+\tbranch, and without making any local branch, and point `HEAD` to\n+\t_<rev>_. When creating a non-bare repository, the revision is checked\n+\tout on a detached `HEAD`. The argument can be a ref name\n+\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n+\tcommit, or a hexadecimal object name.\n+\tThis option is incompatible with `--branch` and `--mirror`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 275940f4f5037de65d0f33eba3e7bc031f9b122b..e66105a62186740845d2014e088ff7cb97ca30f2 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,6 +59,7 @@\n \n struct clone_opts {\n \tint wants_head;\n+\tint detach;\n };\n #define CLONE_OPTS_INIT { \\\n \t.wants_head = 1 /* default enabled */ \\\n@@ -565,11 +566,11 @@ static void update_remote_refs(const struct ref *refs,\n \t}\n }\n \n-static void update_head(const struct ref *our, const struct ref *remote,\n+static void update_head(struct clone_opts *opts, const struct ref *our, const struct ref *remote,\n \t\t\tconst char *unborn, const char *msg)\n {\n \tconst char *head;\n-\tif (our && skip_prefix(our->name, \"refs/heads/\", &head)) {\n+\tif (our && !opts->detach && skip_prefix(our->name, \"refs/heads/\", &head)) {\n \t\t/* Local default branch link */\n \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n \t\t\tdie(_(\"unable to update HEAD\"));\n@@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n \t\t}\n \t} else if (our) {\n-\t\tstruct commit *c = lookup_commit_reference(the_repository,\n-\t\t\t\t\t\t\t   &our->old_oid);\n+\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n+\t\t\t\t\t\t        our->name);\n+\n \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n@@ -900,6 +902,7 @@ int cmd_clone(int argc,\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n+\tchar *option_rev = NULL;\n \n \tstruct clone_opts opts = CLONE_OPTS_INIT;\n \n@@ -943,6 +946,8 @@ int cmd_clone(int argc,\n \t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t\t   N_(\"clone single revision <rev> and check out\")),\n \t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t\t   N_(\"path to git-upload-pack on the remote\")),\n \t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -1279,7 +1284,7 @@ int cmd_clone(int argc,\n \t\tstrbuf_addstr(&branch_top, src_ref_prefix);\n \n \t\tgit_config_set(\"core.bare\", \"true\");\n-\t} else {\n+\t} else if (!option_rev) {\n \t\tstrbuf_addf(&branch_top, \"refs/remotes/%s/\", remote_name);\n \t}\n \n@@ -1298,8 +1303,9 @@ int cmd_clone(int argc,\n \n \tremote = remote_get_early(remote_name);\n \n-\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n-\t\t\tbranch_top.buf);\n+\tif (!option_rev)\n+\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n+\t\t\t\tbranch_top.buf);\n \n \tpath = get_repo_path(remote->url.v[0], &is_bundle);\n \tis_local = option_local != 0 && path && !is_bundle;\n@@ -1342,6 +1348,11 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  !!option_branch, \"--branch\");\n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  option_mirror, \"--mirror\");\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1378,7 +1389,14 @@ int cmd_clone(int argc,\n \tif (transport->smart_options && !deepen && !filter_options.choice)\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n-\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\tif (option_rev) {\n+\t\toption_tags = 0;\n+\t\toption_single_branch = 0;\n+\t\topts.wants_head = 0;\n+\t\topts.detach = 1;\n+\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\t}\n \n \tif (option_tags || option_branch)\n \t\t/*\n@@ -1393,6 +1411,17 @@ int cmd_clone(int argc,\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n \n+\t/*\n+\t * As part of transport_get_remote_refs() the server tells us the hash\n+\t * algorithm, which we require to initialize the repo. But calling that\n+\t * function without any ref prefix, will cause the server to announce\n+\t * all known refs. If the argument passed to --revision was a hex oid,\n+\t * ref_prefixes will be empty so we fall back to asking about HEAD to\n+\t * reduce traffic from the server.\n+\t */\n+\tif (opts.wants_head || transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n \t/*\n@@ -1501,6 +1530,11 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n+\t\tif (!our_head_points_at)\n+\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n+\t\t\t    option_rev, remote_name);\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\n@@ -1539,8 +1573,9 @@ int cmd_clone(int argc,\n \t\tfree(to_free);\n \t}\n \n-\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n-\t\t\tremote_head_points_at, &branch_top);\n+\tif (!option_rev)\n+\t\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n+\t\t\t\t     remote_head_points_at, &branch_top);\n \n \tif (filter_options.choice)\n \t\tpartial_clone_register(remote_name, &filter_options);\n@@ -1556,7 +1591,7 @@ int cmd_clone(int argc,\n \t\t\t   branch_top.buf, reflog_msg.buf, transport,\n \t\t\t   !is_local);\n \n-\tupdate_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n+\tupdate_head(&opts, our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n \n \t/*\n \t * We want to show progress for recursive submodule clones iff\ndiff --git a/t/meson.build b/t/meson.build\nindex 35f25ca4a1d960564190288e9456620a46ccc80a..b5f917926b61de379b6cef45e5f750912422a7d1 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -721,6 +721,7 @@ integration_tests = [\n   't5617-clone-submodules-remote.sh',\n   't5618-alternate-refs.sh',\n   't5619-clone-local-ambiguous-transport.sh',\n+  't5621-clone-revision.sh',\n   't5700-protocol-v1.sh',\n   't5701-git-serve.sh',\n   't5702-protocol-v2.sh',\ndiff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n--- /dev/null\n+++ b/t/t5621-clone-revision.sh\n@@ -0,0 +1,123 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --no-local --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch &&\n+\tgit -C dst rev-list HEAD >actual &&\n+\ttest_line_count = 1 actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0^{} >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision and --bare' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/main --bare . dst &&\n+\toid=$(git rev-parse refs/heads/main) &&\n+\tgit -C dst cat-file -t $oid >actual &&\n+\techo \"commit\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"error: object $oid is a tree, not a commit\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_done\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511860","messageId":"Z6MbPzpGdojU2g2f@pks.im","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 0/7] Enable doing a shallow clone of a specific git revision","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-05T08:03:11Z","receivedAt":"2025-02-05T08:03:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 04, 2025 at 10:33:59PM +0100, Toon Claes wrote:\n> Changes in v5:\n> - Add separate commit to introduce die_for_incompatible_opt2()\n> - Small tweaks in documentation about `--[no-]tags` and `--revision`.\n> - Better explain the refactoring of wanted_peer_refs() in the commit\n>   message.\n> - Change type from `int` to `size_t` in wanted_peer_refs().\n> - Use lookup_commit_or_die() instead lookup_commit_reference() to avoid\n>   checking the result ourself.\n> - Add a few code comments to explain some things.\n> - Stylish cleanups like removal of unneeded empty lines, commented out\n>   test-code and remarks.\n> - Link to v4: https://lore.kernel.org/r/20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com\n\nI've got a couple more nits, but this version looks mostly good to me.\nThanks!\n\nPatrick\n"},{"id":"511861","messageId":"Z6MbSXQ3O476bpfZ@pks.im","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-2-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 2/7] clone: make it possible to specify --tags","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-05T08:03:21Z","receivedAt":"2025-02-05T08:03:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 04, 2025 at 10:34:01PM +0100, Toon Claes wrote:\n> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> index de8d8f58930ecff305f79480b13ddce10cd96c60..e89ae2e8c664f4d4e15e5f05264c8df988295e3d 100644\n> --- a/Documentation/git-clone.txt\n> +++ b/Documentation/git-clone.txt\n> @@ -273,12 +273,13 @@ corresponding `--mirror` and `--no-tags` options instead.\n>  \tbranch when `--single-branch` clone was made, no remote-tracking\n>  \tbranch is created.\n>  \n> -`--no-tags`::\n> -\tDon't clone any tags, and set\n> +`--[no-]tags`::\n> +\tWith `--no-tags`, no tags are cloned, and set\n>  \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n\nThis reads a bit awkward now. How about:\n\n    Control whether or not tags will be cloned. When `--no-tags` is\n    given, the option will be become permanent by setting the\n    `remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n    future `git pull` and `git fetch` won't follow any tags. Subsequent\n    explicit tag fetches will still work (see linkgit:git-fetch[1]).\n\n    By default, tags are cloned and passing `--tags` is thus typically a\n    no-op, unless it cancels out a previous `--no-tags`.\n\n>  \tthat future `git pull` and `git fetch` operations won't follow\n>  \tany tags. Subsequent explicit tag fetches will still work,\n>  \t(see linkgit:git-fetch[1]).\n> +\tBy default tags are cloned, and passing `--tags` doesn't change that.\n\nI think it would make sense to have an empty line before this addition.\n\nPatrick\n"},{"id":"511862","messageId":"Z6MbTVopPjqVi5Al@pks.im","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-4-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 4/7] clone: add tags refspec earlier to fetch refspec","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-05T08:03:25Z","receivedAt":"2025-02-05T08:03:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 04, 2025 at 10:34:03PM +0100, Toon Claes wrote:\n> +\tif (option_tags || option_branch)\n> +\t\t/*\n> +\t\t * Add tags refspec when user asked for tags (implicitly) or\n> +\t\t * specified --branch, which argument might be a tag.\n\ns/which/whose/\n\nPatrick\n"},{"id":"511863","messageId":"Z6MbUbi1A6pTmRdx@pks.im","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-7-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-05T08:03:29Z","receivedAt":"2025-02-05T08:03:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 04, 2025 at 10:34:06PM +0100, Toon Claes wrote:\n> @@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>  \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n>  \t\t}\n>  \t} else if (our) {\n> -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n> -\t\t\t\t\t\t\t   &our->old_oid);\n> +\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n> +\t\t\t\t\t\t        our->name);\n> +\n>  \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n>  \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n>  \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n\nI wonder: is this fixing a potential segfault? If so, it might make\nsense to split this out into a separate commit and provide a test that\ndemonstrates the issue. If it doesn't, then the change shouldn't be\nnecessary, unless I misunderstand.\n\nPatrick\n"},{"id":"511864","messageId":"Z6MbVAwn15_h8Sxy@pks.im","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-6-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 6/7] parse-options: introduce die_for_incompatible_opt2()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-05T08:03:32Z","receivedAt":"2025-02-05T08:03:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 04, 2025 at 10:34:05PM +0100, Toon Claes wrote:\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 1afc6d1ee0cb738fa7fa3f2b5c8ce0dd7802e7da..03d93afd77290ab556565f05bd424956b66ff01c 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -164,7 +164,8 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,\n>  \tif (!rinfo.positive_refexprs)\n>  \t\tdie(_(\"need some commits to replay\"));\n>  \tif (onto_name && *advance_name)\n> -\t\tdie(_(\"--onto and --advance are incompatible\"));\n> +\t\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n> +\t\t\t\t\t  !!*advance_name, \"--advance\");\n\nThe condition isn't needed anymore, is it? As far as I know,\n`die_for_incompatible_opt*()` handle the condition internally.\n\nPatrick\n"},{"id":"511873","messageId":"xmqqtt98pjpf.fsf@gitster.g","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"Re: [PATCH v5 0/7] Enable doing a shallow clone of a specific git revision","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-05T14:09:00Z","receivedAt":"2025-02-05T14:09:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> The goal of this series is to add an option `--revision` to\n> git-clone(1).\n>\n> This series starts with a handful of preparatory refactoring commits\n> that make it more straight-forward to add this new option. In the last\n> commit we're actually adding the feature.\n>\n> This series sets an example on how I think we can further refactor\n> builtin/clone.c to increase the maintainability of the code.\n>\n> ---\n> Changes in v5:\n> - Add separate commit to introduce die_for_incompatible_opt2()\n> - Small tweaks in documentation about `--[no-]tags` and `--revision`.\n> - Better explain the refactoring of wanted_peer_refs() in the commit\n>   message.\n> - Change type from `int` to `size_t` in wanted_peer_refs().\n> - Use lookup_commit_or_die() instead lookup_commit_reference() to avoid\n>   checking the result ourself.\n> - Add a few code comments to explain some things.\n> - Stylish cleanups like removal of unneeded empty lines, commented out\n>   test-code and remarks.\n> - Link to v4: https://lore.kernel.org/r/20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com\n\nLooking good.  Queued.\n"},{"id":"511876","messageId":"87ikpo4apa.fsf@iotcl.com","threadId":"62566","inReplyTo":"Z6MbSXQ3O476bpfZ@pks.im","subject":"Re: [PATCH v5 2/7] clone: make it possible to specify --tags","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:29:05Z","receivedAt":"2025-02-05T16:29:17Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, Feb 04, 2025 at 10:34:01PM +0100, Toon Claes wrote:\n>> diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n>> index de8d8f58930ecff305f79480b13ddce10cd96c60..e89ae2e8c664f4d4e15e5f05264c8df988295e3d 100644\n>> --- a/Documentation/git-clone.txt\n>> +++ b/Documentation/git-clone.txt\n>> @@ -273,12 +273,13 @@ corresponding `--mirror` and `--no-tags` options instead.\n>>  \tbranch when `--single-branch` clone was made, no remote-tracking\n>>  \tbranch is created.\n>>  \n>> -`--no-tags`::\n>> -\tDon't clone any tags, and set\n>> +`--[no-]tags`::\n>> +\tWith `--no-tags`, no tags are cloned, and set\n>>  \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n>\n> This reads a bit awkward now. How about:\n>\n>     Control whether or not tags will be cloned. When `--no-tags` is\n>     given, the option will be become permanent by setting the\n>     `remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n>     future `git pull` and `git fetch` won't follow any tags. Subsequent\n>     explicit tag fetches will still work (see linkgit:git-fetch[1]).\n>\n>     By default, tags are cloned and passing `--tags` is thus typically a\n>     no-op, unless it cancels out a previous `--no-tags`.\n\nSounds good to me, I'll amend my commit.\n\nI'm Cc'ing Jean-Noël to hear their opinion as well.\n\n-- \nToon\n"},{"id":"511878","messageId":"87frks4a1m.fsf@iotcl.com","threadId":"62566","inReplyTo":"Z6MbUbi1A6pTmRdx@pks.im","subject":"Re: [PATCH v5 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:43:17Z","receivedAt":"2025-02-05T16:43:41Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Tue, Feb 04, 2025 at 10:34:06PM +0100, Toon Claes wrote:\n>> @@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>>  \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n>>  \t\t}\n>>  \t} else if (our) {\n>> -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n>> -\t\t\t\t\t\t\t   &our->old_oid);\n>> +\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n>> +\t\t\t\t\t\t        our->name);\n>> +\n>>  \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n>>  \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n>>  \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n>\n> I wonder: is this fixing a potential segfault? If so, it might make\n> sense to split this out into a separate commit and provide a test that\n> demonstrates the issue. If it doesn't, then the change shouldn't be\n> necessary, unless I misunderstand.\n\nThis potential segfault would occur for the added test case:\n'clone with --revision being a tree hash'\n\nSo I can't really split out this change in a separate commit, but the\nchange is still needed.\n\n-- \nToon\n"},{"id":"511879","messageId":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com","subject":"[PATCH v6 0/7] Enable doing a shallow clone of a specific git revision","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:15Z","receivedAt":"2025-02-05T16:47:34Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The goal of this series is to add an option `--revision` to\ngit-clone(1).\n\nThis series starts with a handful of preparatory refactoring commits\nthat make it more straight-forward to add this new option. In the last\ncommit we're actually adding the feature.\n\nThis series sets an example on how I think we can further refactor\nbuiltin/clone.c to increase the maintainability of the code.\n\n---\nChanges in v6:\n- Rewrite the documentation for git-clone(1) --[no-]tags.\n- Remove unneeded conditional around die_for_incompatible_opt2() in\n  builtin/replay.c.\n- Fix typo in code comment in builtin/clone.c.\n- Link to v5: https://lore.kernel.org/r/20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com\n\nChanges in v5:\n- Add separate commit to introduce die_for_incompatible_opt2()\n- Small tweaks in documentation about `--[no-]tags` and `--revision`.\n- Better explain the refactoring of wanted_peer_refs() in the commit\n  message.\n- Change type from `int` to `size_t` in wanted_peer_refs().\n- Use lookup_commit_or_die() instead lookup_commit_reference() to avoid\n  checking the result ourself.\n- Add a few code comments to explain some things.\n- Stylish cleanups like removal of unneeded empty lines, commented out\n  test-code and remarks.\n- Link to v4: https://lore.kernel.org/r/20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com\n\nChanges in v4:\n- Introduce a new commit to reduce the use of global variables.\n- Introduce a new commit to invert the flag --no-tags to --tags.\n- Introduce a new commit to refactor wanted_peer_refs() in\n  builtin/clone.c.\n- Introduce a new commit to shuffle the handling of tags refspec.\n- Introduce a new commit to introduce a `struct clone_opts`.\n- Link to v3: https://lore.kernel.org/r/20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com\n\nChanges in v3:\n- Fail early when the revision was not found on the remote, instead of\n  creating a clone that's in an invalid state.\n- State more clearly in the commit message adding this option is useful\n  for a not uncommon use-case.\n- Be explicit in the documentation the ref needs to peel down to a\n  commit.\n- Die in case we try to update_head() to an object that's not a commit.\n- Allow combining `--revision` with `--bare`.\n- Add die_for_incompatible_opt2() to parse-options.h and use it for the\n  options that are not compatible with the new `--revision` option.\n- Small tweaks to the added tests.\n- Small touchups on commit messages.\n- Link to v2: https://lore.kernel.org/r/20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com\n\n---\nToon Claes (7):\n      clone: cut down on global variables in clone.c\n      clone: make it possible to specify --tags\n      clone: refactor wanted_peer_refs()\n      clone: add tags refspec earlier to fetch refspec\n      clone: introduce struct clone_opts in builtin/clone.c\n      parse-options: introduce die_for_incompatible_opt2()\n      builtin/clone: teach git-clone(1) the --revision= option\n\n Documentation/git-clone.txt |  27 +++-\n builtin/clone.c             | 350 +++++++++++++++++++++++++-------------------\n builtin/replay.c            |   7 +-\n parse-options.h             |   9 ++\n remote.c                    |   2 +-\n remote.h                    |   5 +\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++\n 8 files changed, 359 insertions(+), 165 deletions(-)\n---\n\nRange-diff versus v5:\n\n1:  fa3f2f1f03 = 1:  7d4d3bcc70 clone: cut down on global variables in clone.c\n2:  f88780b503 ! 2:  284f2fc20f clone: make it possible to specify --tags\n    @@ Documentation/git-clone.txt: corresponding `--mirror` and `--no-tags` options in\n      \n     -`--no-tags`::\n     -\tDon't clone any tags, and set\n    +-\t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n    +-\tthat future `git pull` and `git fetch` operations won't follow\n    +-\tany tags. Subsequent explicit tag fetches will still work,\n    +-\t(see linkgit:git-fetch[1]).\n     +`--[no-]tags`::\n    -+\tWith `--no-tags`, no tags are cloned, and set\n    - \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n    - \tthat future `git pull` and `git fetch` operations won't follow\n    - \tany tags. Subsequent explicit tag fetches will still work,\n    - \t(see linkgit:git-fetch[1]).\n    -+\tBy default tags are cloned, and passing `--tags` doesn't change that.\n    ++\tControl whether or not tags will be cloned. When `--no-tags` is\n    ++\tgiven, the option will be become permanent by setting the\n    ++\t`remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n    ++\tfuture `git pull` and `git fetch` won't follow any tags. Subsequent\n    ++\texplicit tag fetches will still work (see linkgit:git-fetch[1]).\n    ++\n    ++\tBy default, tags are cloned and passing `--tags` is thus typically a\n    ++\tno-op, unless it cancels out a previous `--no-tags`.\n      +\n      Can be used in conjunction with `--single-branch` to clone and\n      maintain a branch with no references other than a single cloned\n3:  1289437661 = 3:  9114e6123d clone: refactor wanted_peer_refs()\n4:  9687626855 ! 4:  52807dfdba clone: add tags refspec earlier to fetch refspec\n    @@ builtin/clone.c: int cmd_clone(int argc,\n     +\tif (option_tags || option_branch)\n     +\t\t/*\n     +\t\t * Add tags refspec when user asked for tags (implicitly) or\n    -+\t\t * specified --branch, which argument might be a tag.\n    ++\t\t * specified --branch, whose argument might be a tag.\n     +\t\t */\n     +\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n     +\n5:  c95f9d4f1b = 5:  3a80a479a6 clone: introduce struct clone_opts in builtin/clone.c\n6:  96f92993fd ! 6:  5ef027f7a1 parse-options: introduce die_for_incompatible_opt2()\n    @@ Commit message\n     \n      ## builtin/replay.c ##\n     @@ builtin/replay.c: static void determine_replay_mode(struct rev_cmdline_info *cmd_info,\n    + \tget_ref_information(cmd_info, &rinfo);\n      \tif (!rinfo.positive_refexprs)\n      \t\tdie(_(\"need some commits to replay\"));\n    - \tif (onto_name && *advance_name)\n    +-\tif (onto_name && *advance_name)\n     -\t\tdie(_(\"--onto and --advance are incompatible\"));\n    -+\t\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n    -+\t\t\t\t\t  !!*advance_name, \"--advance\");\n    - \telse if (onto_name) {\n    +-\telse if (onto_name) {\n    ++\n    ++\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n    ++\t\t\t\t  !!*advance_name, \"--advance\");\n    ++\tif (onto_name) {\n      \t\t*onto = peel_committish(onto_name);\n      \t\tif (rinfo.positive_refexprs <\n    + \t\t    strset_get_size(&rinfo.positive_refs))\n     \n      ## parse-options.h ##\n     @@ parse-options.h: static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n7:  9163c9bc85 = 7:  2eaae62e16 builtin/clone: teach git-clone(1) the --revision= option\n\n\n---\n\nbase-commit: bc204b742735ae06f65bb20291c95985c9633b7f\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nThanks\n--\nToon\n\n"},{"id":"511880","messageId":"20250205-toon-clone-refs-v6-1-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 1/7] clone: cut down on global variables in clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:16Z","receivedAt":"2025-02-05T16:47:37Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c the `struct option` which is used to parse the input options\nfor git-clone(1) is a global variable. Due to this, many variables that\nare used to parse the value into, are also global.\n\nMake `builtin_clone_options` a local variable in cmd_clone() and carry\nalong all variables that are only used in that function.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 195 +++++++++++++++++++++++++++++---------------------------\n 1 file changed, 101 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd001d800c635e46bbc7027a8fdda2a8c9fbf069..5ed0802f1d0ddebaf512aac93bf8c8b340494323 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -56,42 +56,22 @@\n  *  - dropping use-separate-remote and no-separate-remote compatibility\n  *\n  */\n-static const char * const builtin_clone_usage[] = {\n-\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n-\tNULL\n-};\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_no_tags;\n static int option_shallow_submodules;\n-static int option_reject_shallow = -1;    /* unspecified */\n static int config_reject_shallow = -1;    /* unspecified */\n-static int deepen;\n-static char *option_template, *option_depth, *option_since;\n-static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n-static struct string_list option_not = STRING_LIST_INIT_NODUP;\n-static const char *real_git_dir;\n-static const char *ref_format;\n-static const char *option_upload_pack = \"git-upload-pack\";\n static int option_verbosity;\n-static int option_progress = -1;\n-static int option_sparse_checkout;\n-static enum transport_family family;\n-static struct string_list option_config = STRING_LIST_INIT_NODUP;\n static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n-static int option_dissociate;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n-static int option_filter_submodules = -1;    /* unspecified */\n static int config_filter_submodules = -1;    /* unspecified */\n-static struct string_list server_options = STRING_LIST_INIT_NODUP;\n static int option_remote_submodules;\n-static const char *bundle_uri;\n \n static int recurse_submodules_cb(const struct option *opt,\n \t\t\t\t const char *arg, int unset)\n@@ -107,78 +87,6 @@ static int recurse_submodules_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static struct option builtin_clone_options[] = {\n-\tOPT__VERBOSITY(&option_verbosity),\n-\tOPT_BOOL(0, \"progress\", &option_progress,\n-\t\t N_(\"force progress reporting\")),\n-\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n-\t\t N_(\"don't clone shallow repository\")),\n-\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n-\t\t N_(\"don't create a checkout\")),\n-\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n-\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n-\t\t\tN_(\"create a bare repository\")),\n-\tOPT_BOOL(0, \"mirror\", &option_mirror,\n-\t\t N_(\"create a mirror repository (implies --bare)\")),\n-\tOPT_BOOL('l', \"local\", &option_local,\n-\t\tN_(\"to clone from a local repository\")),\n-\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n-\t\t    N_(\"don't use local hardlinks, always copy\")),\n-\tOPT_BOOL('s', \"shared\", &option_shared,\n-\t\t    N_(\"setup as shared repository\")),\n-\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n-\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules cloned in parallel\")),\n-\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n-\t\t   N_(\"directory from which templates will be used\")),\n-\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n-\t\t\tN_(\"reference repository\")),\n-\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n-\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n-\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n-\t\t N_(\"use --reference only while cloning\")),\n-\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n-\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n-\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n-\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n-\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to git-upload-pack on the remote\")),\n-\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n-\t\t    N_(\"create a shallow clone of that depth\")),\n-\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n-\t\t    N_(\"create a shallow clone since a specific time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n-\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n-\t\t    N_(\"clone only one branch, HEAD or --branch\")),\n-\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n-\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n-\t\t    N_(\"any cloned submodules will be shallow\")),\n-\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n-\t\t   N_(\"separate git dir from working tree\")),\n-\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n-\t\t   N_(\"specify the reference format to use\")),\n-\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n-\t\t\tN_(\"set config inside the new repository\")),\n-\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n-\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_IPVERSION(&family),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n-\t\t    N_(\"apply partial clone filters to submodules\")),\n-\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n-\t\t    N_(\"any cloned submodules will use their remote-tracking branch\")),\n-\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n-\t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n-\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n-\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n-\tOPT_END()\n-};\n-\n static const char *get_repo_path_1(struct strbuf *path, int *is_bundle)\n {\n \tstatic const char *suffix[] = { \"/.git\", \"\", \".git/.git\", \".git\" };\n@@ -989,10 +897,103 @@ int cmd_clone(int argc,\n \tint hash_algo;\n \tenum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n \tconst int do_not_override_repo_unix_permissions = -1;\n+\tint option_reject_shallow = -1; /* unspecified */\n+\tint deepen = 0;\n+\tchar *option_template = NULL, *option_depth = NULL, *option_since = NULL;\n+\tchar *option_origin = NULL;\n+\tstruct string_list option_not = STRING_LIST_INIT_NODUP;\n+\tconst char *real_git_dir = NULL;\n+\tconst char *ref_format = NULL;\n+\tconst char *option_upload_pack = \"git-upload-pack\";\n+\tint option_progress = -1;\n+\tint option_sparse_checkout = 0;\n+\tenum transport_family family = TRANSPORT_FAMILY_ALL;\n+\tstruct string_list option_config = STRING_LIST_INIT_DUP;\n+\tint option_dissociate = 0;\n+\tint option_filter_submodules = -1; /* unspecified */\n+\tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n+\tconst char *bundle_uri = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n+\tstruct option builtin_clone_options[] = {\n+\t\tOPT__VERBOSITY(&option_verbosity),\n+\t\tOPT_BOOL(0, \"progress\", &option_progress,\n+\t\t\t N_(\"force progress reporting\")),\n+\t\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n+\t\t\t N_(\"don't clone shallow repository\")),\n+\t\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n+\t\t\t N_(\"don't create a checkout\")),\n+\t\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n+\t\t\t\tN_(\"create a bare repository\")),\n+\t\tOPT_BOOL(0, \"mirror\", &option_mirror,\n+\t\t\t N_(\"create a mirror repository (implies --bare)\")),\n+\t\tOPT_BOOL('l', \"local\", &option_local,\n+\t\t\t N_(\"to clone from a local repository\")),\n+\t\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n+\t\t\t N_(\"don't use local hardlinks, always copy\")),\n+\t\tOPT_BOOL('s', \"shared\", &option_shared,\n+\t\t\t N_(\"setup as shared repository\")),\n+\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n+\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n+\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules cloned in parallel\")),\n+\t\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n+\t\t\t   N_(\"directory from which templates will be used\")),\n+\t\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n+\t\t\t\tN_(\"reference repository\")),\n+\t\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n+\t\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n+\t\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n+\t\t\t N_(\"use --reference only while cloning\")),\n+\t\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n+\t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n+\t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to git-upload-pack on the remote\")),\n+\t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n+\t\t\t   N_(\"create a shallow clone of that depth\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n+\t\t\t   N_(\"create a shallow clone since a specific time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n+\t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n+\t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n+\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n+\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n+\t\t\t N_(\"any cloned submodules will be shallow\")),\n+\t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n+\t\t\t   N_(\"separate git dir from working tree\")),\n+\t\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n+\t\t\t   N_(\"specify the reference format to use\")),\n+\t\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n+\t\t\t\tN_(\"set config inside the new repository\")),\n+\t\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_IPVERSION(&family),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n+\t\t\t N_(\"apply partial clone filters to submodules\")),\n+\t\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n+\t\t\t N_(\"any cloned submodules will use their remote-tracking branch\")),\n+\t\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n+\t\t\t N_(\"initialize sparse-checkout file to include only files at root\")),\n+\t\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n+\t\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tconst char * const builtin_clone_usage[] = {\n+\t\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n+\t\tNULL\n+\t};\n+\n \tpacket_trace_identity(\"clone\");\n \n \tgit_config(git_clone_config, NULL);\n@@ -1138,8 +1139,8 @@ int cmd_clone(int argc,\n \t\tfor_each_string_list_item(item, &option_recurse_submodules) {\n \t\t\tstrbuf_addf(&sb, \"submodule.active=%s\",\n \t\t\t\t    item->string);\n-\t\t\tstring_list_append(&option_config,\n-\t\t\t\t\t   strbuf_detach(&sb, NULL));\n+\t\t\tstring_list_append(&option_config, sb.buf);\n+\t\t\tstrbuf_reset(&sb);\n \t\t}\n \n \t\tif (!git_config_get_bool(\"submodule.stickyRecursiveClone\", &val) &&\n@@ -1161,6 +1162,8 @@ int cmd_clone(int argc,\n \t\t\tstring_list_append(&option_config,\n \t\t\t\t\"submodule.alternateErrorStrategy=info\");\n \t\t}\n+\n+\t\tstrbuf_release(&sb);\n \t}\n \n \t/*\n@@ -1578,6 +1581,10 @@ int cmd_clone(int argc,\n \terr = checkout(submodule_progress, filter_submodules,\n \t\t       ref_storage_format);\n \n+\tstring_list_clear(&option_not, 0);\n+\tstring_list_clear(&option_config, 0);\n+\tstring_list_clear(&server_options, 0);\n+\n \tfree(remote_name);\n \tstrbuf_release(&reflog_msg);\n \tstrbuf_release(&branch_top);\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511881","messageId":"20250205-toon-clone-refs-v6-2-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 2/7] clone: make it possible to specify --tags","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:17Z","receivedAt":"2025-02-05T16:47:41Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --no-tags was added in 0dab2468ee (clone: add a --no-tags option\nto clone without tags, 2017-04-26). At the time there was no need to\nsupport --tags as well, although there was some conversation about\nit[1].\n\nTo simplify the code and to prepare for future commits, invert the flag\ninternally. Functionally there is no change, because the flag is\ndefault-enabled passing `--tags` has no effect, so there's no need to\nadd tests for this.\n\n[1]: https://lore.kernel.org/git/CAGZ79kbHuMpiavJ90kQLEL_AR0BEyArcZoEWAjPPhOFacN16YQ@mail.gmail.com/\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt | 17 ++++++++++-------\n builtin/clone.c             | 14 +++++++-------\n 2 files changed, 17 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex de8d8f58930ecff305f79480b13ddce10cd96c60..8d0476f6dcaf6fed7ccd48a20398556dd4e20722 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -13,7 +13,7 @@ git clone [--template=<template-directory>]\n \t  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]\n \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]\n \t  [--dissociate] [--separate-git-dir <git-dir>]\n-\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n+\t  [--depth <depth>] [--[no-]single-branch] [--[no-]tags]\n \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]\n \t  [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository>\n@@ -273,12 +273,15 @@ corresponding `--mirror` and `--no-tags` options instead.\n \tbranch when `--single-branch` clone was made, no remote-tracking\n \tbranch is created.\n \n-`--no-tags`::\n-\tDon't clone any tags, and set\n-\t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n-\tthat future `git pull` and `git fetch` operations won't follow\n-\tany tags. Subsequent explicit tag fetches will still work,\n-\t(see linkgit:git-fetch[1]).\n+`--[no-]tags`::\n+\tControl whether or not tags will be cloned. When `--no-tags` is\n+\tgiven, the option will be become permanent by setting the\n+\t`remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n+\tfuture `git pull` and `git fetch` won't follow any tags. Subsequent\n+\texplicit tag fetches will still work (see linkgit:git-fetch[1]).\n+\n+\tBy default, tags are cloned and passing `--tags` is thus typically a\n+\tno-op, unless it cancels out a previous `--no-tags`.\n +\n Can be used in conjunction with `--single-branch` to clone and\n maintain a branch with no references other than a single cloned\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5ed0802f1d0ddebaf512aac93bf8c8b340494323..69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,7 +59,7 @@\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n-static int option_no_tags;\n+static int option_tags = 1; /* default enabled */\n static int option_shallow_submodules;\n static int config_reject_shallow = -1;    /* unspecified */\n static char *remote_name = NULL;\n@@ -470,7 +470,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && !option_no_tags)\n+\tif (!option_mirror && !option_single_branch && option_tags)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n \trefspec_item_clear(&tag_refspec);\n@@ -562,7 +562,7 @@ static void update_remote_refs(const struct ref *refs,\n \n \tif (refs) {\n \t\twrite_remote_refs(mapped_refs);\n-\t\tif (option_single_branch && !option_no_tags)\n+\t\tif (option_single_branch && option_tags)\n \t\t\twrite_followtags(refs, msg);\n \t}\n \n@@ -964,8 +964,8 @@ int cmd_clone(int argc,\n \t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n \t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n \t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n-\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"tags\", &option_tags,\n+\t\t\t N_(\"clone tags, and make later fetches not to follow them\")),\n \t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n \t\t\t N_(\"any cloned submodules will be shallow\")),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n@@ -1296,7 +1296,7 @@ int cmd_clone(int argc,\n \tgit_config_set(key.buf, repo);\n \tstrbuf_reset(&key);\n \n-\tif (option_no_tags) {\n+\tif (!option_tags) {\n \t\tstrbuf_addf(&key, \"remote.%s.tagOpt\", remote_name);\n \t\tgit_config_set(key.buf, \"--no-tags\");\n \t\tstrbuf_reset(&key);\n@@ -1389,7 +1389,7 @@ int cmd_clone(int argc,\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (!option_no_tags)\n+\tif (option_tags)\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511882","messageId":"20250205-toon-clone-refs-v6-3-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 3/7] clone: refactor wanted_peer_refs()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:18Z","receivedAt":"2025-02-05T16:47:45Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The function wanted_peer_refs() is used to map the refs returned by the\nserver to refs we will save in our clone.\n\nOver time this function grown to be very complex. Refactor it.\n\nPreviously, there was a separate code path for when\n`option_single_branch` was set. It resulted in duplicated code and\ndeeper nested conditions. After this refactor the code path for when\n`option_single_branch` is truthy modifies `refs` and then falls through\nto the common code path. This approach relies on the `refspec` being set\ncorrectly and thus only mapping refs that are relevant.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 39 +++++++++++++++------------------------\n 1 file changed, 15 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4..5efa2bbceb42b230f723660ea963ca1253888235 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -434,46 +434,37 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n {\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n-\tstruct ref **tail = head ? &head->next : &local_refs;\n+\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n \tstruct refspec_item tag_refspec;\n+\tstruct ref *to_free = NULL;\n \n \trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n \n \tif (option_single_branch) {\n-\t\tstruct ref *remote_head = NULL;\n-\n \t\tif (!option_branch)\n-\t\t\tremote_head = guess_remote_head(head, refs, 0);\n+\t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n \t\telse {\n \t\t\tfree_one_ref(head);\n \t\t\tlocal_refs = head = NULL;\n \t\t\ttail = &local_refs;\n-\t\t\tremote_head = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n-\n-\t\tif (!remote_head && option_branch)\n-\t\t\twarning(_(\"Could not find remote branch %s to clone.\"),\n-\t\t\t\toption_branch);\n-\t\telse {\n-\t\t\tint i;\n-\t\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\t\tget_fetch_map(remote_head, &refspec->items[i],\n-\t\t\t\t\t      &tail, 0);\n-\n-\t\t\t/* if --branch=tag, pull the requested tag explicitly */\n-\t\t\tget_fetch_map(remote_head, &tag_refspec, &tail, 0);\n+\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t\t}\n-\t\tfree_refs(remote_head);\n-\t} else {\n-\t\tint i;\n-\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && option_tags)\n+\tfor (size_t i = 0; i < refspec->nr; i++)\n+\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n+\n+\t/*\n+\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n+\t * about won't be present in `refs` anyway.\n+\t * Except with option --mirror, where we grab all refs already.\n+\t */\n+\tif (!option_mirror)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n+\tfree_one_ref(to_free);\n \trefspec_item_clear(&tag_refspec);\n+\n \treturn local_refs;\n }\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511883","messageId":"20250205-toon-clone-refs-v6-4-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 4/7] clone: add tags refspec earlier to fetch refspec","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:19Z","receivedAt":"2025-02-05T16:47:49Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\nfrom the `remote->fetch` refspec into `ref_prefixes` of\n`transport_ls_refs_options`. Afterwards we add the tags prefix\n`refs/tags/` prefix as well. At a later point, in wanted_peer_refs() we\nprocess refs using both `remote->fetch` and `TAG_REFSPEC`.\n\nSimplify the code by appending `TAG_REFSPEC` to `remote->fetch` before\ncalling refspec_ref_prefixes().\n\nTo be able to do this, we set `option_tags` to 0 when --mirror is given.\nThis is because --mirror mirrors (hence the name) all the refs,\nincluding tags and they do not need to be treated separately.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5efa2bbceb42b230f723660ea963ca1253888235..ef4af1f3e6bf0a33c13ed874587b35aba14fe43c 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -435,11 +435,8 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n \tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n-\tstruct refspec_item tag_refspec;\n \tstruct ref *to_free = NULL;\n \n-\trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n-\n \tif (option_single_branch) {\n \t\tif (!option_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n@@ -454,16 +451,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tfor (size_t i = 0; i < refspec->nr; i++)\n \t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \n-\t/*\n-\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n-\t * about won't be present in `refs` anyway.\n-\t * Except with option --mirror, where we grab all refs already.\n-\t */\n-\tif (!option_mirror)\n-\t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n-\n \tfree_one_ref(to_free);\n-\trefspec_item_clear(&tag_refspec);\n \n \treturn local_refs;\n }\n@@ -1011,8 +999,10 @@ int cmd_clone(int argc,\n \t\t\tdie(_(\"unknown ref storage format '%s'\"), ref_format);\n \t}\n \n-\tif (option_mirror)\n+\tif (option_mirror) {\n \t\toption_bare = 1;\n+\t\toption_tags = 0;\n+\t}\n \n \tif (option_bare) {\n \t\tif (real_git_dir)\n@@ -1375,14 +1365,19 @@ int cmd_clone(int argc,\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n+\tif (option_tags || option_branch)\n+\t\t/*\n+\t\t * Add tags refspec when user asked for tags (implicitly) or\n+\t\t * specified --branch, whose argument might be a tag.\n+\t\t */\n+\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n+\n \trefspec_ref_prefixes(&remote->fetch,\n \t\t\t     &transport_ls_refs_options.ref_prefixes);\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (option_tags)\n-\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n-\t\t\t    \"refs/tags/\");\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511884","messageId":"20250205-toon-clone-refs-v6-5-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 5/7] clone: introduce struct clone_opts in builtin/clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:20Z","receivedAt":"2025-02-05T16:47:57Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"There is a lot of state stored in global variables in builtin/clone.c.\nIn the long run we'd like to remove many of those.\n\nIntroduce `struct clone_opts` in this file. This struct will be used to\ncontain all details needed to perform the clone. The struct object can\nbe thrown around to all the functions that need these details.\n\nThe first field we're adding is `wants_head`. In some scenarios\n(specifically when both `--single-branch` and `--branch` are given) we\nare not interested in `HEAD` on the remote. The field `wants_head` in\n`struct clone_opts` will hold this information. We could have put\n`option_branch` and `option_single_branch` into that struct instead, but\nin a following commit we'll be using `wants_head` as well.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 44 +++++++++++++++++++++++++++++---------------\n remote.c        |  2 +-\n remote.h        |  5 +++++\n 3 files changed, 35 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex ef4af1f3e6bf0a33c13ed874587b35aba14fe43c..1d421c8f758e37a7219d2da680c7ef8699016171 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -57,6 +57,13 @@\n  *\n  */\n \n+struct clone_opts {\n+\tint wants_head;\n+};\n+#define CLONE_OPTS_INIT { \\\n+\t.wants_head = 1 /* default enabled */ \\\n+}\n+\n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_tags = 1; /* default enabled */\n@@ -429,23 +436,24 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch\n \treturn ref;\n }\n \n-static struct ref *wanted_peer_refs(const struct ref *refs,\n-\t\tstruct refspec *refspec)\n+static struct ref *wanted_peer_refs(struct clone_opts *opts,\n+\t\t\t\t    const struct ref *refs,\n+\t\t\t\t    struct refspec *refspec)\n {\n-\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n-\tstruct ref *local_refs = head;\n-\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n+\tstruct ref *local_refs = NULL;\n+\tstruct ref **tail = &local_refs;\n \tstruct ref *to_free = NULL;\n \n-\tif (option_single_branch) {\n-\t\tif (!option_branch)\n+\tif (opts->wants_head) {\n+\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n+\t\tif (head)\n+\t\t\ttail_link_ref(head, &tail);\n+\t\tif (option_single_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n-\t\telse {\n-\t\t\tfree_one_ref(head);\n-\t\t\tlocal_refs = head = NULL;\n-\t\t\ttail = &local_refs;\n-\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n+\t} else if (option_single_branch) {\n+\t\tlocal_refs = NULL;\n+\t\ttail = &local_refs;\n+\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t}\n \n \tfor (size_t i = 0; i < refspec->nr; i++)\n@@ -893,6 +901,8 @@ int cmd_clone(int argc,\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n \n+\tstruct clone_opts opts = CLONE_OPTS_INIT;\n+\n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n@@ -1343,9 +1353,13 @@ int cmd_clone(int argc,\n \tif (option_not.nr)\n \t\ttransport_set_option(transport, TRANS_OPT_DEEPEN_NOT,\n \t\t\t\t     (const char *)&option_not);\n-\tif (option_single_branch)\n+\tif (option_single_branch) {\n \t\ttransport_set_option(transport, TRANS_OPT_FOLLOWTAGS, \"1\");\n \n+\t\tif (option_branch)\n+\t\t\topts.wants_head = 0;\n+\t}\n+\n \tif (option_upload_pack)\n \t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK,\n \t\t\t\t     option_upload_pack);\n@@ -1454,7 +1468,7 @@ int cmd_clone(int argc,\n \t}\n \n \tif (refs)\n-\t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t\tmapped_refs = wanted_peer_refs(&opts, refs, &remote->fetch);\n \n \tif (mapped_refs) {\n \t\t/*\ndiff --git a/remote.c b/remote.c\nindex 1779f0e7bbb8b88965f2fedf407e50ed20ea7a13..69d8c43ea694f1b9a7699e5d5a49bfc169058b8e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1260,7 +1260,7 @@ int count_refspec_match(const char *pattern,\n \t}\n }\n \n-static void tail_link_ref(struct ref *ref, struct ref ***tail)\n+void tail_link_ref(struct ref *ref, struct ref ***tail)\n {\n \t**tail = ref;\n \twhile (ref->next)\ndiff --git a/remote.h b/remote.h\nindex a19353f68999f5440db7bf5f91dd4be8bcc1d8a5..ce3e7c8512981d0ac5db2da508c4fbe64cede961 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -221,6 +221,11 @@ struct ref *alloc_ref(const char *name);\n struct ref *copy_ref(const struct ref *ref);\n struct ref *copy_ref_list(const struct ref *ref);\n int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n+/*\n+ * Put a ref in the tail and prepare tail for adding another one.\n+ * *tail is the pointer to the tail of the list of refs.\n+ */\n+void tail_link_ref(struct ref *ref, struct ref ***tail);\n \n int check_ref_type(const struct ref *ref, int flags);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511885","messageId":"20250205-toon-clone-refs-v6-6-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 6/7] parse-options: introduce die_for_incompatible_opt2()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:21Z","receivedAt":"2025-02-05T16:47:59Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The functions die_for_incompatible_opt3() and\ndie_for_incompatible_opt4() already exist to die whenever a user\nspecifies three or four options respectively that are not compatible.\n\nIntroduce die_for_incompatible_opt2() which dies when two options that\nare incompatible are set.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/replay.c | 7 ++++---\n parse-options.h  | 9 +++++++++\n 2 files changed, 13 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1afc6d1ee0cb738fa7fa3f2b5c8ce0dd7802e7da..032c172b65ece4e2c3b275ffdde22a54c55a3933 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -163,9 +163,10 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,\n \tget_ref_information(cmd_info, &rinfo);\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n-\tif (onto_name && *advance_name)\n-\t\tdie(_(\"--onto and --advance are incompatible\"));\n-\telse if (onto_name) {\n+\n+\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n+\t\t\t\t  !!*advance_name, \"--advance\");\n+\tif (onto_name) {\n \t\t*onto = peel_committish(onto_name);\n \t\tif (rinfo.positive_refexprs <\n \t\t    strset_get_size(&rinfo.positive_refs))\ndiff --git a/parse-options.h b/parse-options.h\nindex 39f088625494f20dea96b9a9cbe986916773bf60..fca944d9a93d643d984c58de2ead9154c8b16c94 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -436,6 +436,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n \t\t\t\t  0, \"\");\n }\n \n+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n+\t\t\t\t\t     int opt2, const char *opt2_name)\n+{\n+\tdie_for_incompatible_opt4(opt1, opt1_name,\n+\t\t\t\t  opt2, opt2_name,\n+\t\t\t\t  0, \"\",\n+\t\t\t\t  0, \"\");\n+}\n+\n /*\n  * Use these assertions for callbacks that expect to be called with NONEG and\n  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511886","messageId":"20250205-toon-clone-refs-v6-7-0bbc8e6d89fd@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v6 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-05T16:47:22Z","receivedAt":"2025-02-05T16:48:07Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot be selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a hexadecimal commit ID. This enables the user\nto clone and check out any revision they want. `--revision` can be used\nin conjunction with `--depth` to do a minimal clone that only contains\nthe blob and tree for a single revision. This can be useful for\nautomated tests running in CI systems.\n\nUsing option `--branch` and `--single-branch` together is a similar\nscenario, but serves a different purpose. Using these two options, a\nsinglet remote tracking branch is created and the fetch refspec is set\nup so git-fetch(1) will receive updates on that branch from the remote.\nThis allows the user work on that single branch.\n\nOption `--revision` on contrary detaches HEAD, creates no tracking\nbranches, and writes no fetch refspec.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |  10 ++++\n builtin/clone.c             |  57 ++++++++++++++++----\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 180 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex 8d0476f6dcaf6fed7ccd48a20398556dd4e20722..f9cefbf2f643415be11ed0c7f9b460517830bc0f 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -221,6 +221,16 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision=<rev>`::\n+\tCreate a new repository, and fetch the history leading to the given\n+\trevision _<rev>_ (and nothing else), without making any remote-tracking\n+\tbranch, and without making any local branch, and point `HEAD` to\n+\t_<rev>_. When creating a non-bare repository, the revision is checked\n+\tout on a detached `HEAD`. The argument can be a ref name\n+\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n+\tcommit, or a hexadecimal object name.\n+\tThis option is incompatible with `--branch` and `--mirror`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 1d421c8f758e37a7219d2da680c7ef8699016171..6ad041e288bc7a926413124442d4f285eea7ad95 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,6 +59,7 @@\n \n struct clone_opts {\n \tint wants_head;\n+\tint detach;\n };\n #define CLONE_OPTS_INIT { \\\n \t.wants_head = 1 /* default enabled */ \\\n@@ -565,11 +566,11 @@ static void update_remote_refs(const struct ref *refs,\n \t}\n }\n \n-static void update_head(const struct ref *our, const struct ref *remote,\n+static void update_head(struct clone_opts *opts, const struct ref *our, const struct ref *remote,\n \t\t\tconst char *unborn, const char *msg)\n {\n \tconst char *head;\n-\tif (our && skip_prefix(our->name, \"refs/heads/\", &head)) {\n+\tif (our && !opts->detach && skip_prefix(our->name, \"refs/heads/\", &head)) {\n \t\t/* Local default branch link */\n \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n \t\t\tdie(_(\"unable to update HEAD\"));\n@@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n \t\t}\n \t} else if (our) {\n-\t\tstruct commit *c = lookup_commit_reference(the_repository,\n-\t\t\t\t\t\t\t   &our->old_oid);\n+\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n+\t\t\t\t\t\t        our->name);\n+\n \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n@@ -900,6 +902,7 @@ int cmd_clone(int argc,\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n+\tchar *option_rev = NULL;\n \n \tstruct clone_opts opts = CLONE_OPTS_INIT;\n \n@@ -943,6 +946,8 @@ int cmd_clone(int argc,\n \t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t\t   N_(\"clone single revision <rev> and check out\")),\n \t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t\t   N_(\"path to git-upload-pack on the remote\")),\n \t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -1279,7 +1284,7 @@ int cmd_clone(int argc,\n \t\tstrbuf_addstr(&branch_top, src_ref_prefix);\n \n \t\tgit_config_set(\"core.bare\", \"true\");\n-\t} else {\n+\t} else if (!option_rev) {\n \t\tstrbuf_addf(&branch_top, \"refs/remotes/%s/\", remote_name);\n \t}\n \n@@ -1298,8 +1303,9 @@ int cmd_clone(int argc,\n \n \tremote = remote_get_early(remote_name);\n \n-\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n-\t\t\tbranch_top.buf);\n+\tif (!option_rev)\n+\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n+\t\t\t\tbranch_top.buf);\n \n \tpath = get_repo_path(remote->url.v[0], &is_bundle);\n \tis_local = option_local != 0 && path && !is_bundle;\n@@ -1342,6 +1348,11 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  !!option_branch, \"--branch\");\n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  option_mirror, \"--mirror\");\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1378,7 +1389,14 @@ int cmd_clone(int argc,\n \tif (transport->smart_options && !deepen && !filter_options.choice)\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n-\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\tif (option_rev) {\n+\t\toption_tags = 0;\n+\t\toption_single_branch = 0;\n+\t\topts.wants_head = 0;\n+\t\topts.detach = 1;\n+\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\t}\n \n \tif (option_tags || option_branch)\n \t\t/*\n@@ -1393,6 +1411,17 @@ int cmd_clone(int argc,\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n \n+\t/*\n+\t * As part of transport_get_remote_refs() the server tells us the hash\n+\t * algorithm, which we require to initialize the repo. But calling that\n+\t * function without any ref prefix, will cause the server to announce\n+\t * all known refs. If the argument passed to --revision was a hex oid,\n+\t * ref_prefixes will be empty so we fall back to asking about HEAD to\n+\t * reduce traffic from the server.\n+\t */\n+\tif (opts.wants_head || transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n \t/*\n@@ -1501,6 +1530,11 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n+\t\tif (!our_head_points_at)\n+\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n+\t\t\t    option_rev, remote_name);\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\n@@ -1539,8 +1573,9 @@ int cmd_clone(int argc,\n \t\tfree(to_free);\n \t}\n \n-\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n-\t\t\tremote_head_points_at, &branch_top);\n+\tif (!option_rev)\n+\t\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n+\t\t\t\t     remote_head_points_at, &branch_top);\n \n \tif (filter_options.choice)\n \t\tpartial_clone_register(remote_name, &filter_options);\n@@ -1556,7 +1591,7 @@ int cmd_clone(int argc,\n \t\t\t   branch_top.buf, reflog_msg.buf, transport,\n \t\t\t   !is_local);\n \n-\tupdate_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n+\tupdate_head(&opts, our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n \n \t/*\n \t * We want to show progress for recursive submodule clones iff\ndiff --git a/t/meson.build b/t/meson.build\nindex 35f25ca4a1d960564190288e9456620a46ccc80a..b5f917926b61de379b6cef45e5f750912422a7d1 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -721,6 +721,7 @@ integration_tests = [\n   't5617-clone-submodules-remote.sh',\n   't5618-alternate-refs.sh',\n   't5619-clone-local-ambiguous-transport.sh',\n+  't5621-clone-revision.sh',\n   't5700-protocol-v1.sh',\n   't5701-git-serve.sh',\n   't5702-protocol-v2.sh',\ndiff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n--- /dev/null\n+++ b/t/t5621-clone-revision.sh\n@@ -0,0 +1,123 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --no-local --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch &&\n+\tgit -C dst rev-list HEAD >actual &&\n+\ttest_line_count = 1 actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0^{} >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision and --bare' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/main --bare . dst &&\n+\toid=$(git rev-parse refs/heads/main) &&\n+\tgit -C dst cat-file -t $oid >actual &&\n+\techo \"commit\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"error: object $oid is a tree, not a commit\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_done\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511891","messageId":"xmqqbjvgpan9.fsf@gitster.g","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-7-0bbc8e6d89fd@iotcl.com","subject":"Re: [PATCH v6 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-05T17:24:42Z","receivedAt":"2025-02-05T17:24:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Toon Claes <toon@iotcl.com> writes:\n\n> The git-clone(1) command has the option `--branch` that allows the user\n> to select the branch they want HEAD to point to. In a non-bare\n> repository this also checks out that branch.\n>\n> Option `--branch` also accepts a tag. When a tag name is provided, the\n> commit this tag points to is checked out and HEAD is detached. Thus\n> `--branch` can be used to clone a repository and check out a ref kept\n> under `refs/heads` or `refs/tags`. But some other refs might be in use\n> as well. For example Git forges might use refs like `refs/pull/<id>` and\n> `refs/merge-requests/<id>` to track pull/merge requests. These refs\n> cannot be selected upon git-clone(1).\n\nThese examples made the motivation a lot easier to see.  Very nice\ncompared to a much earlier edition.\n\n> +`--revision=<rev>`::\n> +\tCreate a new repository, and fetch the history leading to the given\n> +\trevision _<rev>_ (and nothing else), without making any remote-tracking\n> +\tbranch, and without making any local branch, and point `HEAD` to\n> +\t_<rev>_. When creating a non-bare repository, the revision is checked\n> +\tout on a detached `HEAD`. The argument can be a ref name\n\nMicronit.  I think the resulting repository, even when it is bare,\nwould have its HEAD detached (i.e., instead of being a symbolic ref\nto a local branch, points directly at a commit object).\n\n\t... branch, and detach the `HEAD` to _<rev>_.  When creating\n\ta non-bare repository, the revision is checked out.  The\n\targument can be a ref name ...\n\nBut then \"When ... checked out\" probably goes without saying.  There\nis nothing special wrt to bare/non-bare this option affects the\nbehaviour of the command.\n\n> @@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n>  \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n>  \t\t}\n>  \t} else if (our) {\n> -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n> -\t\t\t\t\t\t\t   &our->old_oid);\n> +\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n> +\t\t\t\t\t\t        our->name);\n> +\n\n.git/rebase-apply/patch:62: indent with spaces.\n\t\t\t\t\t\t        our->name);\nwarning: 1 line applied after fixing whitespace errors.\n\nOther than that, looking good.\n\nThanks.\n"},{"id":"511908","messageId":"2212047.irdbgypaU6@cayenne","threadId":"62566","inReplyTo":"Z6MbSXQ3O476bpfZ@pks.im","subject":"Re: [PATCH v5 2/7] clone: make it possible to specify --tags","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-02-05T21:15:14Z","receivedAt":"2025-02-05T21:15:28Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Wednesday, 5 February 2025 09:03:21 UTC+1 Patrick Steinhardt wrote:\n> On Tue, Feb 04, 2025 at 10:34:01PM +0100, Toon Claes wrote:\n> > diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\n> > index\n> > de8d8f58930ecff305f79480b13ddce10cd96c60..e89ae2e8c664f4d4e15e5f05264c8df98\n> > 8295e3d 100644 --- a/Documentation/git-clone.txt\n> > +++ b/Documentation/git-clone.txt\n> > @@ -273,12 +273,13 @@ corresponding `--mirror` and `--no-tags` options\n> > instead.> \n> >  \tbranch when `--single-branch` clone was made, no remote-tracking\n> >  \tbranch is created.\n> > \n> > -`--no-tags`::\n> > -\tDon't clone any tags, and set\n> > +`--[no-]tags`::\n> > +\tWith `--no-tags`, no tags are cloned, and set\n> > \n> >  \t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n> \n> This reads a bit awkward now. How about:\n> \n>     Control whether or not tags will be cloned. When `--no-tags` is\nThe future form is useless, as it applies to the current command run.\n\n>     given, the option will be become permanent by setting the\n\n\"will be become permanent\": I do not understand this part. If I parse it \ncorrectly, this a passive future perfect form, but it does not relate to the \ncurrent state.\n\n>     `remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n>     future `git pull` and `git fetch` won't follow any tags. Subsequent\n>     explicit tag fetches will still work (see linkgit:git-fetch[1]).\n> \n>     By default, tags are cloned and passing `--tags` is thus typically a\n>     no-op, unless it cancels out a previous `--no-tags`.\n> \n\nIf you let an empty new line followed by an indented paragraph, this creates a \ncitation paragraph. What you want is to attach it, like so:\n\n\t(...)\n\texplicit tag fetches will still work (see linkgit:git-fetch[1]).\n+\nBy default, tags are cloned and passing `--tags` is thus typically a\nno-op, unless it cancels out a previous `--no-tags`.\n\n\nJN\n\n\n\n"},{"id":"511936","messageId":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com","subject":"[PATCH v7 0/7] Enable doing a shallow clone of a specific git revision","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:28Z","receivedAt":"2025-02-06T06:33:48Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The goal of this series is to add an option `--revision` to\ngit-clone(1).\n\nThis series starts with a handful of preparatory refactoring commits\nthat make it more straight-forward to add this new option. In the last\ncommit we're actually adding the feature.\n\nThis series sets an example on how I think we can further refactor\nbuiltin/clone.c to increase the maintainability of the code.\n\n---\nChanges in v7:\n- Further enhance documentation of option --revision on git-clone(1).\n- Indentation fix in builtin/clone.c.\n- Link to v6: https://lore.kernel.org/r/20250205-toon-clone-refs-v6-0-0bbc8e6d89fd@iotcl.com\n\nChanges in v6:\n- Rewrite the documentation for git-clone(1) --[no-]tags.\n- Remove unneeded conditional around die_for_incompatible_opt2() in\n  builtin/replay.c.\n- Fix typo in code comment in builtin/clone.c.\n- Link to v5: https://lore.kernel.org/r/20250204-toon-clone-refs-v5-0-37e34af283c8@iotcl.com\n\nChanges in v5:\n- Add separate commit to introduce die_for_incompatible_opt2()\n- Small tweaks in documentation about `--[no-]tags` and `--revision`.\n- Better explain the refactoring of wanted_peer_refs() in the commit\n  message.\n- Change type from `int` to `size_t` in wanted_peer_refs().\n- Use lookup_commit_or_die() instead lookup_commit_reference() to avoid\n  checking the result ourself.\n- Add a few code comments to explain some things.\n- Stylish cleanups like removal of unneeded empty lines, commented out\n  test-code and remarks.\n- Link to v4: https://lore.kernel.org/r/20250131-toon-clone-refs-v4-0-2a4ff851498f@iotcl.com\n\nChanges in v4:\n- Introduce a new commit to reduce the use of global variables.\n- Introduce a new commit to invert the flag --no-tags to --tags.\n- Introduce a new commit to refactor wanted_peer_refs() in\n  builtin/clone.c.\n- Introduce a new commit to shuffle the handling of tags refspec.\n- Introduce a new commit to introduce a `struct clone_opts`.\n- Link to v3: https://lore.kernel.org/r/20241219-toon-clone-refs-v3-1-1484faea3008@iotcl.com\n\nChanges in v3:\n- Fail early when the revision was not found on the remote, instead of\n  creating a clone that's in an invalid state.\n- State more clearly in the commit message adding this option is useful\n  for a not uncommon use-case.\n- Be explicit in the documentation the ref needs to peel down to a\n  commit.\n- Die in case we try to update_head() to an object that's not a commit.\n- Allow combining `--revision` with `--bare`.\n- Add die_for_incompatible_opt2() to parse-options.h and use it for the\n  options that are not compatible with the new `--revision` option.\n- Small tweaks to the added tests.\n- Small touchups on commit messages.\n- Link to v2: https://lore.kernel.org/r/20241129-toon-clone-refs-v2-1-dca4c19a3510@iotcl.com\n\n---\nToon Claes (7):\n      clone: cut down on global variables in clone.c\n      clone: make it possible to specify --tags\n      clone: refactor wanted_peer_refs()\n      clone: add tags refspec earlier to fetch refspec\n      clone: introduce struct clone_opts in builtin/clone.c\n      parse-options: introduce die_for_incompatible_opt2()\n      builtin/clone: teach git-clone(1) the --revision= option\n\n Documentation/git-clone.txt |  26 +++-\n builtin/clone.c             | 350 +++++++++++++++++++++++++-------------------\n builtin/replay.c            |   7 +-\n parse-options.h             |   9 ++\n remote.c                    |   2 +-\n remote.h                    |   5 +\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++\n 8 files changed, 358 insertions(+), 165 deletions(-)\n---\n\nRange-diff versus v6:\n\n1:  14e4210ef9 = 1:  e9d0d1bb4e clone: cut down on global variables in clone.c\n2:  f85beed07e = 2:  5c570e08f6 clone: make it possible to specify --tags\n3:  92b998a173 = 3:  bb5d206ee6 clone: refactor wanted_peer_refs()\n4:  3fb2766728 = 4:  344a2f143c clone: add tags refspec earlier to fetch refspec\n5:  d0341cad24 = 5:  93d074d17e clone: introduce struct clone_opts in builtin/clone.c\n6:  d42f291e48 = 6:  457f21943e parse-options: introduce die_for_incompatible_opt2()\n7:  3f332971dc ! 7:  fb4f05547e builtin/clone: teach git-clone(1) the --revision= option\n    @@ Documentation/git-clone.txt: objects from the source repository into a pack in t\n     +`--revision=<rev>`::\n     +\tCreate a new repository, and fetch the history leading to the given\n     +\trevision _<rev>_ (and nothing else), without making any remote-tracking\n    -+\tbranch, and without making any local branch, and point `HEAD` to\n    -+\t_<rev>_. When creating a non-bare repository, the revision is checked\n    -+\tout on a detached `HEAD`. The argument can be a ref name\n    -+\t(e.g. `refs/heads/main` or `refs/tags/v1.0`) that peels down to a\n    -+\tcommit, or a hexadecimal object name.\n    ++\tbranch, and without making any local branch, and detach `HEAD` to\n    ++\t_<rev>_. The argument can be a ref name (e.g. `refs/heads/main` or\n    ++\t`refs/tags/v1.0`) that peels down to a commit, or a hexadecimal object\n    ++\tname.\n     +\tThis option is incompatible with `--branch` and `--mirror`.\n     +\n      `-u` _<upload-pack>_::\n    @@ builtin/clone.c: static void update_head(const struct ref *our, const struct ref\n     -\t\tstruct commit *c = lookup_commit_reference(the_repository,\n     -\t\t\t\t\t\t\t   &our->old_oid);\n     +\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n    -+\t\t\t\t\t\t        our->name);\n    ++\t\t\t\t\t\t\tour->name);\n     +\n      \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n      \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n\n\n---\n\nbase-commit: bc204b742735ae06f65bb20291c95985c9633b7f\nchange-id: 20241129-toon-clone-refs-ad3623772f92\n\nThanks\n--\nToon\n\n"},{"id":"511937","messageId":"20250206-toon-clone-refs-v7-1-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 1/7] clone: cut down on global variables in clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:29Z","receivedAt":"2025-02-06T06:33:51Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c the `struct option` which is used to parse the input options\nfor git-clone(1) is a global variable. Due to this, many variables that\nare used to parse the value into, are also global.\n\nMake `builtin_clone_options` a local variable in cmd_clone() and carry\nalong all variables that are only used in that function.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 195 +++++++++++++++++++++++++++++---------------------------\n 1 file changed, 101 insertions(+), 94 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd001d800c635e46bbc7027a8fdda2a8c9fbf069..5ed0802f1d0ddebaf512aac93bf8c8b340494323 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -56,42 +56,22 @@\n  *  - dropping use-separate-remote and no-separate-remote compatibility\n  *\n  */\n-static const char * const builtin_clone_usage[] = {\n-\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n-\tNULL\n-};\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_no_tags;\n static int option_shallow_submodules;\n-static int option_reject_shallow = -1;    /* unspecified */\n static int config_reject_shallow = -1;    /* unspecified */\n-static int deepen;\n-static char *option_template, *option_depth, *option_since;\n-static char *option_origin = NULL;\n static char *remote_name = NULL;\n static char *option_branch = NULL;\n-static struct string_list option_not = STRING_LIST_INIT_NODUP;\n-static const char *real_git_dir;\n-static const char *ref_format;\n-static const char *option_upload_pack = \"git-upload-pack\";\n static int option_verbosity;\n-static int option_progress = -1;\n-static int option_sparse_checkout;\n-static enum transport_family family;\n-static struct string_list option_config = STRING_LIST_INIT_NODUP;\n static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;\n static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n-static int option_dissociate;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;\n-static int option_filter_submodules = -1;    /* unspecified */\n static int config_filter_submodules = -1;    /* unspecified */\n-static struct string_list server_options = STRING_LIST_INIT_NODUP;\n static int option_remote_submodules;\n-static const char *bundle_uri;\n \n static int recurse_submodules_cb(const struct option *opt,\n \t\t\t\t const char *arg, int unset)\n@@ -107,78 +87,6 @@ static int recurse_submodules_cb(const struct option *opt,\n \treturn 0;\n }\n \n-static struct option builtin_clone_options[] = {\n-\tOPT__VERBOSITY(&option_verbosity),\n-\tOPT_BOOL(0, \"progress\", &option_progress,\n-\t\t N_(\"force progress reporting\")),\n-\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n-\t\t N_(\"don't clone shallow repository\")),\n-\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n-\t\t N_(\"don't create a checkout\")),\n-\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n-\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n-\t\t\tN_(\"create a bare repository\")),\n-\tOPT_BOOL(0, \"mirror\", &option_mirror,\n-\t\t N_(\"create a mirror repository (implies --bare)\")),\n-\tOPT_BOOL('l', \"local\", &option_local,\n-\t\tN_(\"to clone from a local repository\")),\n-\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n-\t\t    N_(\"don't use local hardlinks, always copy\")),\n-\tOPT_BOOL('s', \"shared\", &option_shared,\n-\t\t    N_(\"setup as shared repository\")),\n-\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n-\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n-\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n-\t\t    N_(\"number of submodules cloned in parallel\")),\n-\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n-\t\t   N_(\"directory from which templates will be used\")),\n-\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n-\t\t\tN_(\"reference repository\")),\n-\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n-\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n-\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n-\t\t N_(\"use --reference only while cloning\")),\n-\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n-\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n-\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n-\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n-\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n-\t\t   N_(\"path to git-upload-pack on the remote\")),\n-\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n-\t\t    N_(\"create a shallow clone of that depth\")),\n-\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n-\t\t    N_(\"create a shallow clone since a specific time\")),\n-\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n-\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n-\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n-\t\t    N_(\"clone only one branch, HEAD or --branch\")),\n-\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n-\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n-\t\t    N_(\"any cloned submodules will be shallow\")),\n-\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n-\t\t   N_(\"separate git dir from working tree\")),\n-\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n-\t\t   N_(\"specify the reference format to use\")),\n-\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n-\t\t\tN_(\"set config inside the new repository\")),\n-\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n-\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n-\tOPT_IPVERSION(&family),\n-\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n-\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n-\t\t    N_(\"apply partial clone filters to submodules\")),\n-\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n-\t\t    N_(\"any cloned submodules will use their remote-tracking branch\")),\n-\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n-\t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n-\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n-\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n-\tOPT_END()\n-};\n-\n static const char *get_repo_path_1(struct strbuf *path, int *is_bundle)\n {\n \tstatic const char *suffix[] = { \"/.git\", \"\", \".git/.git\", \".git\" };\n@@ -989,10 +897,103 @@ int cmd_clone(int argc,\n \tint hash_algo;\n \tenum ref_storage_format ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n \tconst int do_not_override_repo_unix_permissions = -1;\n+\tint option_reject_shallow = -1; /* unspecified */\n+\tint deepen = 0;\n+\tchar *option_template = NULL, *option_depth = NULL, *option_since = NULL;\n+\tchar *option_origin = NULL;\n+\tstruct string_list option_not = STRING_LIST_INIT_NODUP;\n+\tconst char *real_git_dir = NULL;\n+\tconst char *ref_format = NULL;\n+\tconst char *option_upload_pack = \"git-upload-pack\";\n+\tint option_progress = -1;\n+\tint option_sparse_checkout = 0;\n+\tenum transport_family family = TRANSPORT_FAMILY_ALL;\n+\tstruct string_list option_config = STRING_LIST_INIT_DUP;\n+\tint option_dissociate = 0;\n+\tint option_filter_submodules = -1; /* unspecified */\n+\tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n+\tconst char *bundle_uri = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n+\tstruct option builtin_clone_options[] = {\n+\t\tOPT__VERBOSITY(&option_verbosity),\n+\t\tOPT_BOOL(0, \"progress\", &option_progress,\n+\t\t\t N_(\"force progress reporting\")),\n+\t\tOPT_BOOL(0, \"reject-shallow\", &option_reject_shallow,\n+\t\t\t N_(\"don't clone shallow repository\")),\n+\t\tOPT_BOOL('n', \"no-checkout\", &option_no_checkout,\n+\t\t\t N_(\"don't create a checkout\")),\n+\t\tOPT_BOOL(0, \"bare\", &option_bare, N_(\"create a bare repository\")),\n+\t\tOPT_HIDDEN_BOOL(0, \"naked\", &option_bare,\n+\t\t\t\tN_(\"create a bare repository\")),\n+\t\tOPT_BOOL(0, \"mirror\", &option_mirror,\n+\t\t\t N_(\"create a mirror repository (implies --bare)\")),\n+\t\tOPT_BOOL('l', \"local\", &option_local,\n+\t\t\t N_(\"to clone from a local repository\")),\n+\t\tOPT_BOOL(0, \"no-hardlinks\", &option_no_hardlinks,\n+\t\t\t N_(\"don't use local hardlinks, always copy\")),\n+\t\tOPT_BOOL('s', \"shared\", &option_shared,\n+\t\t\t N_(\"setup as shared repository\")),\n+\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n+\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n+\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n+\t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n+\t\t\t    N_(\"number of submodules cloned in parallel\")),\n+\t\tOPT_STRING(0, \"template\", &option_template, N_(\"template-directory\"),\n+\t\t\t   N_(\"directory from which templates will be used\")),\n+\t\tOPT_STRING_LIST(0, \"reference\", &option_required_reference, N_(\"repo\"),\n+\t\t\t\tN_(\"reference repository\")),\n+\t\tOPT_STRING_LIST(0, \"reference-if-able\", &option_optional_reference,\n+\t\t\t\tN_(\"repo\"), N_(\"reference repository\")),\n+\t\tOPT_BOOL(0, \"dissociate\", &option_dissociate,\n+\t\t\t N_(\"use --reference only while cloning\")),\n+\t\tOPT_STRING('o', \"origin\", &option_origin, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n+\t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n+\t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n+\t\t\t   N_(\"path to git-upload-pack on the remote\")),\n+\t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n+\t\t\t   N_(\"create a shallow clone of that depth\")),\n+\t\tOPT_STRING(0, \"shallow-since\", &option_since, N_(\"time\"),\n+\t\t\t   N_(\"create a shallow clone since a specific time\")),\n+\t\tOPT_STRING_LIST(0, \"shallow-exclude\", &option_not, N_(\"ref\"),\n+\t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n+\t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n+\t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n+\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n+\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n+\t\t\t N_(\"any cloned submodules will be shallow\")),\n+\t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n+\t\t\t   N_(\"separate git dir from working tree\")),\n+\t\tOPT_STRING(0, \"ref-format\", &ref_format, N_(\"format\"),\n+\t\t\t   N_(\"specify the reference format to use\")),\n+\t\tOPT_STRING_LIST('c', \"config\", &option_config, N_(\"key=value\"),\n+\t\t\t\tN_(\"set config inside the new repository\")),\n+\t\tOPT_STRING_LIST(0, \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"), N_(\"option to transmit\")),\n+\t\tOPT_IPVERSION(&family),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n+\t\tOPT_BOOL(0, \"also-filter-submodules\", &option_filter_submodules,\n+\t\t\t N_(\"apply partial clone filters to submodules\")),\n+\t\tOPT_BOOL(0, \"remote-submodules\", &option_remote_submodules,\n+\t\t\t N_(\"any cloned submodules will use their remote-tracking branch\")),\n+\t\tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n+\t\t\t N_(\"initialize sparse-checkout file to include only files at root\")),\n+\t\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n+\t\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tconst char * const builtin_clone_usage[] = {\n+\t\tN_(\"git clone [<options>] [--] <repo> [<dir>]\"),\n+\t\tNULL\n+\t};\n+\n \tpacket_trace_identity(\"clone\");\n \n \tgit_config(git_clone_config, NULL);\n@@ -1138,8 +1139,8 @@ int cmd_clone(int argc,\n \t\tfor_each_string_list_item(item, &option_recurse_submodules) {\n \t\t\tstrbuf_addf(&sb, \"submodule.active=%s\",\n \t\t\t\t    item->string);\n-\t\t\tstring_list_append(&option_config,\n-\t\t\t\t\t   strbuf_detach(&sb, NULL));\n+\t\t\tstring_list_append(&option_config, sb.buf);\n+\t\t\tstrbuf_reset(&sb);\n \t\t}\n \n \t\tif (!git_config_get_bool(\"submodule.stickyRecursiveClone\", &val) &&\n@@ -1161,6 +1162,8 @@ int cmd_clone(int argc,\n \t\t\tstring_list_append(&option_config,\n \t\t\t\t\"submodule.alternateErrorStrategy=info\");\n \t\t}\n+\n+\t\tstrbuf_release(&sb);\n \t}\n \n \t/*\n@@ -1578,6 +1581,10 @@ int cmd_clone(int argc,\n \terr = checkout(submodule_progress, filter_submodules,\n \t\t       ref_storage_format);\n \n+\tstring_list_clear(&option_not, 0);\n+\tstring_list_clear(&option_config, 0);\n+\tstring_list_clear(&server_options, 0);\n+\n \tfree(remote_name);\n \tstrbuf_release(&reflog_msg);\n \tstrbuf_release(&branch_top);\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511938","messageId":"20250206-toon-clone-refs-v7-2-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 2/7] clone: make it possible to specify --tags","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:30Z","receivedAt":"2025-02-06T06:33:56Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Option --no-tags was added in 0dab2468ee (clone: add a --no-tags option\nto clone without tags, 2017-04-26). At the time there was no need to\nsupport --tags as well, although there was some conversation about\nit[1].\n\nTo simplify the code and to prepare for future commits, invert the flag\ninternally. Functionally there is no change, because the flag is\ndefault-enabled passing `--tags` has no effect, so there's no need to\nadd tests for this.\n\n[1]: https://lore.kernel.org/git/CAGZ79kbHuMpiavJ90kQLEL_AR0BEyArcZoEWAjPPhOFacN16YQ@mail.gmail.com/\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt | 17 ++++++++++-------\n builtin/clone.c             | 14 +++++++-------\n 2 files changed, 17 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex de8d8f58930ecff305f79480b13ddce10cd96c60..8d0476f6dcaf6fed7ccd48a20398556dd4e20722 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -13,7 +13,7 @@ git clone [--template=<template-directory>]\n \t  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]\n \t  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]\n \t  [--dissociate] [--separate-git-dir <git-dir>]\n-\t  [--depth <depth>] [--[no-]single-branch] [--no-tags]\n+\t  [--depth <depth>] [--[no-]single-branch] [--[no-]tags]\n \t  [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]\n \t  [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]\n \t  [--filter=<filter-spec>] [--also-filter-submodules]] [--] <repository>\n@@ -273,12 +273,15 @@ corresponding `--mirror` and `--no-tags` options instead.\n \tbranch when `--single-branch` clone was made, no remote-tracking\n \tbranch is created.\n \n-`--no-tags`::\n-\tDon't clone any tags, and set\n-\t`remote.<remote>.tagOpt=--no-tags` in the config, ensuring\n-\tthat future `git pull` and `git fetch` operations won't follow\n-\tany tags. Subsequent explicit tag fetches will still work,\n-\t(see linkgit:git-fetch[1]).\n+`--[no-]tags`::\n+\tControl whether or not tags will be cloned. When `--no-tags` is\n+\tgiven, the option will be become permanent by setting the\n+\t`remote.<remote>.tagOpt=--no-tags` configuration. This ensures that\n+\tfuture `git pull` and `git fetch` won't follow any tags. Subsequent\n+\texplicit tag fetches will still work (see linkgit:git-fetch[1]).\n+\n+\tBy default, tags are cloned and passing `--tags` is thus typically a\n+\tno-op, unless it cancels out a previous `--no-tags`.\n +\n Can be used in conjunction with `--single-branch` to clone and\n maintain a branch with no references other than a single cloned\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5ed0802f1d0ddebaf512aac93bf8c8b340494323..69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,7 +59,7 @@\n \n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n-static int option_no_tags;\n+static int option_tags = 1; /* default enabled */\n static int option_shallow_submodules;\n static int config_reject_shallow = -1;    /* unspecified */\n static char *remote_name = NULL;\n@@ -470,7 +470,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && !option_no_tags)\n+\tif (!option_mirror && !option_single_branch && option_tags)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n \trefspec_item_clear(&tag_refspec);\n@@ -562,7 +562,7 @@ static void update_remote_refs(const struct ref *refs,\n \n \tif (refs) {\n \t\twrite_remote_refs(mapped_refs);\n-\t\tif (option_single_branch && !option_no_tags)\n+\t\tif (option_single_branch && option_tags)\n \t\t\twrite_followtags(refs, msg);\n \t}\n \n@@ -964,8 +964,8 @@ int cmd_clone(int argc,\n \t\t\t\tN_(\"deepen history of shallow clone, excluding ref\")),\n \t\tOPT_BOOL(0, \"single-branch\", &option_single_branch,\n \t\t\t N_(\"clone only one branch, HEAD or --branch\")),\n-\t\tOPT_BOOL(0, \"no-tags\", &option_no_tags,\n-\t\t\t N_(\"don't clone any tags, and make later fetches not to follow them\")),\n+\t\tOPT_BOOL(0, \"tags\", &option_tags,\n+\t\t\t N_(\"clone tags, and make later fetches not to follow them\")),\n \t\tOPT_BOOL(0, \"shallow-submodules\", &option_shallow_submodules,\n \t\t\t N_(\"any cloned submodules will be shallow\")),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n@@ -1296,7 +1296,7 @@ int cmd_clone(int argc,\n \tgit_config_set(key.buf, repo);\n \tstrbuf_reset(&key);\n \n-\tif (option_no_tags) {\n+\tif (!option_tags) {\n \t\tstrbuf_addf(&key, \"remote.%s.tagOpt\", remote_name);\n \t\tgit_config_set(key.buf, \"--no-tags\");\n \t\tstrbuf_reset(&key);\n@@ -1389,7 +1389,7 @@ int cmd_clone(int argc,\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (!option_no_tags)\n+\tif (option_tags)\n \t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n \t\t\t    \"refs/tags/\");\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511939","messageId":"20250206-toon-clone-refs-v7-3-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 3/7] clone: refactor wanted_peer_refs()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:31Z","receivedAt":"2025-02-06T06:34:04Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The function wanted_peer_refs() is used to map the refs returned by the\nserver to refs we will save in our clone.\n\nOver time this function grown to be very complex. Refactor it.\n\nPreviously, there was a separate code path for when\n`option_single_branch` was set. It resulted in duplicated code and\ndeeper nested conditions. After this refactor the code path for when\n`option_single_branch` is truthy modifies `refs` and then falls through\nto the common code path. This approach relies on the `refspec` being set\ncorrectly and thus only mapping refs that are relevant.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 39 +++++++++++++++------------------------\n 1 file changed, 15 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 69d1ad029dfa84a2f7136fa4a3c4c8a594b179c4..5efa2bbceb42b230f723660ea963ca1253888235 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -434,46 +434,37 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n {\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n-\tstruct ref **tail = head ? &head->next : &local_refs;\n+\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n \tstruct refspec_item tag_refspec;\n+\tstruct ref *to_free = NULL;\n \n \trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n \n \tif (option_single_branch) {\n-\t\tstruct ref *remote_head = NULL;\n-\n \t\tif (!option_branch)\n-\t\t\tremote_head = guess_remote_head(head, refs, 0);\n+\t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n \t\telse {\n \t\t\tfree_one_ref(head);\n \t\t\tlocal_refs = head = NULL;\n \t\t\ttail = &local_refs;\n-\t\t\tremote_head = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n-\n-\t\tif (!remote_head && option_branch)\n-\t\t\twarning(_(\"Could not find remote branch %s to clone.\"),\n-\t\t\t\toption_branch);\n-\t\telse {\n-\t\t\tint i;\n-\t\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\t\tget_fetch_map(remote_head, &refspec->items[i],\n-\t\t\t\t\t      &tail, 0);\n-\n-\t\t\t/* if --branch=tag, pull the requested tag explicitly */\n-\t\t\tget_fetch_map(remote_head, &tag_refspec, &tail, 0);\n+\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t\t}\n-\t\tfree_refs(remote_head);\n-\t} else {\n-\t\tint i;\n-\t\tfor (i = 0; i < refspec->nr; i++)\n-\t\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \t}\n \n-\tif (!option_mirror && !option_single_branch && option_tags)\n+\tfor (size_t i = 0; i < refspec->nr; i++)\n+\t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n+\n+\t/*\n+\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n+\t * about won't be present in `refs` anyway.\n+\t * Except with option --mirror, where we grab all refs already.\n+\t */\n+\tif (!option_mirror)\n \t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n \n+\tfree_one_ref(to_free);\n \trefspec_item_clear(&tag_refspec);\n+\n \treturn local_refs;\n }\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511940","messageId":"20250206-toon-clone-refs-v7-4-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 4/7] clone: add tags refspec earlier to fetch refspec","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:32Z","receivedAt":"2025-02-06T06:34:07Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"In clone.c we call refspec_ref_prefixes() to copy the fetch refspecs\nfrom the `remote->fetch` refspec into `ref_prefixes` of\n`transport_ls_refs_options`. Afterwards we add the tags prefix\n`refs/tags/` prefix as well. At a later point, in wanted_peer_refs() we\nprocess refs using both `remote->fetch` and `TAG_REFSPEC`.\n\nSimplify the code by appending `TAG_REFSPEC` to `remote->fetch` before\ncalling refspec_ref_prefixes().\n\nTo be able to do this, we set `option_tags` to 0 when --mirror is given.\nThis is because --mirror mirrors (hence the name) all the refs,\nincluding tags and they do not need to be treated separately.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 27 +++++++++++----------------\n 1 file changed, 11 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 5efa2bbceb42b230f723660ea963ca1253888235..ef4af1f3e6bf0a33c13ed874587b35aba14fe43c 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -435,11 +435,8 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n \tstruct ref *local_refs = head;\n \tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n-\tstruct refspec_item tag_refspec;\n \tstruct ref *to_free = NULL;\n \n-\trefspec_item_init(&tag_refspec, TAG_REFSPEC, 0);\n-\n \tif (option_single_branch) {\n \t\tif (!option_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n@@ -454,16 +451,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs,\n \tfor (size_t i = 0; i < refspec->nr; i++)\n \t\tget_fetch_map(refs, &refspec->items[i], &tail, 0);\n \n-\t/*\n-\t * Grab all refs that match the TAG_REFSPEC. Any tags we don't care\n-\t * about won't be present in `refs` anyway.\n-\t * Except with option --mirror, where we grab all refs already.\n-\t */\n-\tif (!option_mirror)\n-\t\tget_fetch_map(refs, &tag_refspec, &tail, 0);\n-\n \tfree_one_ref(to_free);\n-\trefspec_item_clear(&tag_refspec);\n \n \treturn local_refs;\n }\n@@ -1011,8 +999,10 @@ int cmd_clone(int argc,\n \t\t\tdie(_(\"unknown ref storage format '%s'\"), ref_format);\n \t}\n \n-\tif (option_mirror)\n+\tif (option_mirror) {\n \t\toption_bare = 1;\n+\t\toption_tags = 0;\n+\t}\n \n \tif (option_bare) {\n \t\tif (real_git_dir)\n@@ -1375,14 +1365,19 @@ int cmd_clone(int argc,\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n \tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n+\tif (option_tags || option_branch)\n+\t\t/*\n+\t\t * Add tags refspec when user asked for tags (implicitly) or\n+\t\t * specified --branch, whose argument might be a tag.\n+\t\t */\n+\t\trefspec_append(&remote->fetch, TAG_REFSPEC);\n+\n \trefspec_ref_prefixes(&remote->fetch,\n \t\t\t     &transport_ls_refs_options.ref_prefixes);\n \tif (option_branch)\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n-\tif (option_tags)\n-\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n-\t\t\t    \"refs/tags/\");\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511941","messageId":"20250206-toon-clone-refs-v7-5-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 5/7] clone: introduce struct clone_opts in builtin/clone.c","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:33Z","receivedAt":"2025-02-06T06:34:11Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"There is a lot of state stored in global variables in builtin/clone.c.\nIn the long run we'd like to remove many of those.\n\nIntroduce `struct clone_opts` in this file. This struct will be used to\ncontain all details needed to perform the clone. The struct object can\nbe thrown around to all the functions that need these details.\n\nThe first field we're adding is `wants_head`. In some scenarios\n(specifically when both `--single-branch` and `--branch` are given) we\nare not interested in `HEAD` on the remote. The field `wants_head` in\n`struct clone_opts` will hold this information. We could have put\n`option_branch` and `option_single_branch` into that struct instead, but\nin a following commit we'll be using `wants_head` as well.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/clone.c | 44 +++++++++++++++++++++++++++++---------------\n remote.c        |  2 +-\n remote.h        |  5 +++++\n 3 files changed, 35 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex ef4af1f3e6bf0a33c13ed874587b35aba14fe43c..1d421c8f758e37a7219d2da680c7ef8699016171 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -57,6 +57,13 @@\n  *\n  */\n \n+struct clone_opts {\n+\tint wants_head;\n+};\n+#define CLONE_OPTS_INIT { \\\n+\t.wants_head = 1 /* default enabled */ \\\n+}\n+\n static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1;\n static int option_local = -1, option_no_hardlinks, option_shared;\n static int option_tags = 1; /* default enabled */\n@@ -429,23 +436,24 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch\n \treturn ref;\n }\n \n-static struct ref *wanted_peer_refs(const struct ref *refs,\n-\t\tstruct refspec *refspec)\n+static struct ref *wanted_peer_refs(struct clone_opts *opts,\n+\t\t\t\t    const struct ref *refs,\n+\t\t\t\t    struct refspec *refspec)\n {\n-\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n-\tstruct ref *local_refs = head;\n-\tstruct ref **tail = local_refs ? &local_refs->next : &local_refs;\n+\tstruct ref *local_refs = NULL;\n+\tstruct ref **tail = &local_refs;\n \tstruct ref *to_free = NULL;\n \n-\tif (option_single_branch) {\n-\t\tif (!option_branch)\n+\tif (opts->wants_head) {\n+\t\tstruct ref *head = copy_ref(find_ref_by_name(refs, \"HEAD\"));\n+\t\tif (head)\n+\t\t\ttail_link_ref(head, &tail);\n+\t\tif (option_single_branch)\n \t\t\trefs = to_free = guess_remote_head(head, refs, 0);\n-\t\telse {\n-\t\t\tfree_one_ref(head);\n-\t\t\tlocal_refs = head = NULL;\n-\t\t\ttail = &local_refs;\n-\t\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n-\t\t}\n+\t} else if (option_single_branch) {\n+\t\tlocal_refs = NULL;\n+\t\ttail = &local_refs;\n+\t\trefs = to_free = copy_ref(find_remote_branch(refs, option_branch));\n \t}\n \n \tfor (size_t i = 0; i < refspec->nr; i++)\n@@ -893,6 +901,8 @@ int cmd_clone(int argc,\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n \n+\tstruct clone_opts opts = CLONE_OPTS_INIT;\n+\n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n \n@@ -1343,9 +1353,13 @@ int cmd_clone(int argc,\n \tif (option_not.nr)\n \t\ttransport_set_option(transport, TRANS_OPT_DEEPEN_NOT,\n \t\t\t\t     (const char *)&option_not);\n-\tif (option_single_branch)\n+\tif (option_single_branch) {\n \t\ttransport_set_option(transport, TRANS_OPT_FOLLOWTAGS, \"1\");\n \n+\t\tif (option_branch)\n+\t\t\topts.wants_head = 0;\n+\t}\n+\n \tif (option_upload_pack)\n \t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK,\n \t\t\t\t     option_upload_pack);\n@@ -1454,7 +1468,7 @@ int cmd_clone(int argc,\n \t}\n \n \tif (refs)\n-\t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n+\t\tmapped_refs = wanted_peer_refs(&opts, refs, &remote->fetch);\n \n \tif (mapped_refs) {\n \t\t/*\ndiff --git a/remote.c b/remote.c\nindex 1779f0e7bbb8b88965f2fedf407e50ed20ea7a13..69d8c43ea694f1b9a7699e5d5a49bfc169058b8e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1260,7 +1260,7 @@ int count_refspec_match(const char *pattern,\n \t}\n }\n \n-static void tail_link_ref(struct ref *ref, struct ref ***tail)\n+void tail_link_ref(struct ref *ref, struct ref ***tail)\n {\n \t**tail = ref;\n \twhile (ref->next)\ndiff --git a/remote.h b/remote.h\nindex a19353f68999f5440db7bf5f91dd4be8bcc1d8a5..ce3e7c8512981d0ac5db2da508c4fbe64cede961 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -221,6 +221,11 @@ struct ref *alloc_ref(const char *name);\n struct ref *copy_ref(const struct ref *ref);\n struct ref *copy_ref_list(const struct ref *ref);\n int count_refspec_match(const char *, struct ref *refs, struct ref **matched_ref);\n+/*\n+ * Put a ref in the tail and prepare tail for adding another one.\n+ * *tail is the pointer to the tail of the list of refs.\n+ */\n+void tail_link_ref(struct ref *ref, struct ref ***tail);\n \n int check_ref_type(const struct ref *ref, int flags);\n \n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511942","messageId":"20250206-toon-clone-refs-v7-6-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 6/7] parse-options: introduce die_for_incompatible_opt2()","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:34Z","receivedAt":"2025-02-06T06:34:17Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The functions die_for_incompatible_opt3() and\ndie_for_incompatible_opt4() already exist to die whenever a user\nspecifies three or four options respectively that are not compatible.\n\nIntroduce die_for_incompatible_opt2() which dies when two options that\nare incompatible are set.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/replay.c | 7 ++++---\n parse-options.h  | 9 +++++++++\n 2 files changed, 13 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1afc6d1ee0cb738fa7fa3f2b5c8ce0dd7802e7da..032c172b65ece4e2c3b275ffdde22a54c55a3933 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -163,9 +163,10 @@ static void determine_replay_mode(struct rev_cmdline_info *cmd_info,\n \tget_ref_information(cmd_info, &rinfo);\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n-\tif (onto_name && *advance_name)\n-\t\tdie(_(\"--onto and --advance are incompatible\"));\n-\telse if (onto_name) {\n+\n+\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n+\t\t\t\t  !!*advance_name, \"--advance\");\n+\tif (onto_name) {\n \t\t*onto = peel_committish(onto_name);\n \t\tif (rinfo.positive_refexprs <\n \t\t    strset_get_size(&rinfo.positive_refs))\ndiff --git a/parse-options.h b/parse-options.h\nindex 39f088625494f20dea96b9a9cbe986916773bf60..fca944d9a93d643d984c58de2ead9154c8b16c94 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -436,6 +436,15 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name,\n \t\t\t\t  0, \"\");\n }\n \n+static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name,\n+\t\t\t\t\t     int opt2, const char *opt2_name)\n+{\n+\tdie_for_incompatible_opt4(opt1, opt1_name,\n+\t\t\t\t  opt2, opt2_name,\n+\t\t\t\t  0, \"\",\n+\t\t\t\t  0, \"\");\n+}\n+\n /*\n  * Use these assertions for callbacks that expect to be called with NONEG and\n  * NOARG respectively, and do not otherwise handle the \"unset\" and \"arg\"\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511943","messageId":"20250206-toon-clone-refs-v7-7-4622b7392202@iotcl.com","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-0-4622b7392202@iotcl.com","subject":"[PATCH v7 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-02-06T06:33:35Z","receivedAt":"2025-02-06T06:34:20Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"The git-clone(1) command has the option `--branch` that allows the user\nto select the branch they want HEAD to point to. In a non-bare\nrepository this also checks out that branch.\n\nOption `--branch` also accepts a tag. When a tag name is provided, the\ncommit this tag points to is checked out and HEAD is detached. Thus\n`--branch` can be used to clone a repository and check out a ref kept\nunder `refs/heads` or `refs/tags`. But some other refs might be in use\nas well. For example Git forges might use refs like `refs/pull/<id>` and\n`refs/merge-requests/<id>` to track pull/merge requests. These refs\ncannot be selected upon git-clone(1).\n\nAdd option `--revision` to git-clone(1). This option accepts a fully\nqualified reference, or a hexadecimal commit ID. This enables the user\nto clone and check out any revision they want. `--revision` can be used\nin conjunction with `--depth` to do a minimal clone that only contains\nthe blob and tree for a single revision. This can be useful for\nautomated tests running in CI systems.\n\nUsing option `--branch` and `--single-branch` together is a similar\nscenario, but serves a different purpose. Using these two options, a\nsinglet remote tracking branch is created and the fetch refspec is set\nup so git-fetch(1) will receive updates on that branch from the remote.\nThis allows the user work on that single branch.\n\nOption `--revision` on contrary detaches HEAD, creates no tracking\nbranches, and writes no fetch refspec.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-clone.txt |   9 ++++\n builtin/clone.c             |  57 ++++++++++++++++----\n t/meson.build               |   1 +\n t/t5621-clone-revision.sh   | 123 ++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 179 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt\nindex 8d0476f6dcaf6fed7ccd48a20398556dd4e20722..1069d56e7126c006c14c6b4579547f79d7fe4fb5 100644\n--- a/Documentation/git-clone.txt\n+++ b/Documentation/git-clone.txt\n@@ -221,6 +221,15 @@ objects from the source repository into a pack in the cloned repository.\n \t`--branch` can also take tags and detaches the `HEAD` at that commit\n \tin the resulting repository.\n \n+`--revision=<rev>`::\n+\tCreate a new repository, and fetch the history leading to the given\n+\trevision _<rev>_ (and nothing else), without making any remote-tracking\n+\tbranch, and without making any local branch, and detach `HEAD` to\n+\t_<rev>_. The argument can be a ref name (e.g. `refs/heads/main` or\n+\t`refs/tags/v1.0`) that peels down to a commit, or a hexadecimal object\n+\tname.\n+\tThis option is incompatible with `--branch` and `--mirror`.\n+\n `-u` _<upload-pack>_::\n `--upload-pack` _<upload-pack>_::\n \tWhen given, and the repository to clone from is accessed\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 1d421c8f758e37a7219d2da680c7ef8699016171..f9a2ecbe9cc944793203d722b42573e779575c5a 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -59,6 +59,7 @@\n \n struct clone_opts {\n \tint wants_head;\n+\tint detach;\n };\n #define CLONE_OPTS_INIT { \\\n \t.wants_head = 1 /* default enabled */ \\\n@@ -565,11 +566,11 @@ static void update_remote_refs(const struct ref *refs,\n \t}\n }\n \n-static void update_head(const struct ref *our, const struct ref *remote,\n+static void update_head(struct clone_opts *opts, const struct ref *our, const struct ref *remote,\n \t\t\tconst char *unborn, const char *msg)\n {\n \tconst char *head;\n-\tif (our && skip_prefix(our->name, \"refs/heads/\", &head)) {\n+\tif (our && !opts->detach && skip_prefix(our->name, \"refs/heads/\", &head)) {\n \t\t/* Local default branch link */\n \t\tif (refs_update_symref(get_main_ref_store(the_repository), \"HEAD\", our->name, NULL) < 0)\n \t\t\tdie(_(\"unable to update HEAD\"));\n@@ -580,8 +581,9 @@ static void update_head(const struct ref *our, const struct ref *remote,\n \t\t\tinstall_branch_config(0, head, remote_name, our->name);\n \t\t}\n \t} else if (our) {\n-\t\tstruct commit *c = lookup_commit_reference(the_repository,\n-\t\t\t\t\t\t\t   &our->old_oid);\n+\t\tstruct commit *c = lookup_commit_or_die(&our->old_oid,\n+\t\t\t\t\t\t\tour->name);\n+\n \t\t/* --branch specifies a non-branch (i.e. tags), detach HEAD */\n \t\trefs_update_ref(get_main_ref_store(the_repository), msg,\n \t\t\t\t\"HEAD\", &c->object.oid, NULL, REF_NO_DEREF,\n@@ -900,6 +902,7 @@ int cmd_clone(int argc,\n \tint option_filter_submodules = -1; /* unspecified */\n \tstruct string_list server_options = STRING_LIST_INIT_NODUP;\n \tconst char *bundle_uri = NULL;\n+\tchar *option_rev = NULL;\n \n \tstruct clone_opts opts = CLONE_OPTS_INIT;\n \n@@ -943,6 +946,8 @@ int cmd_clone(int argc,\n \t\t\t   N_(\"use <name> instead of 'origin' to track upstream\")),\n \t\tOPT_STRING('b', \"branch\", &option_branch, N_(\"branch\"),\n \t\t\t   N_(\"checkout <branch> instead of the remote's HEAD\")),\n+\t\tOPT_STRING(0, \"revision\", &option_rev, N_(\"rev\"),\n+\t\t\t   N_(\"clone single revision <rev> and check out\")),\n \t\tOPT_STRING('u', \"upload-pack\", &option_upload_pack, N_(\"path\"),\n \t\t\t   N_(\"path to git-upload-pack on the remote\")),\n \t\tOPT_STRING(0, \"depth\", &option_depth, N_(\"depth\"),\n@@ -1279,7 +1284,7 @@ int cmd_clone(int argc,\n \t\tstrbuf_addstr(&branch_top, src_ref_prefix);\n \n \t\tgit_config_set(\"core.bare\", \"true\");\n-\t} else {\n+\t} else if (!option_rev) {\n \t\tstrbuf_addf(&branch_top, \"refs/remotes/%s/\", remote_name);\n \t}\n \n@@ -1298,8 +1303,9 @@ int cmd_clone(int argc,\n \n \tremote = remote_get_early(remote_name);\n \n-\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n-\t\t\tbranch_top.buf);\n+\tif (!option_rev)\n+\t\trefspec_appendf(&remote->fetch, \"+%s*:%s*\", src_ref_prefix,\n+\t\t\t\tbranch_top.buf);\n \n \tpath = get_repo_path(remote->url.v[0], &is_bundle);\n \tis_local = option_local != 0 && path && !is_bundle;\n@@ -1342,6 +1348,11 @@ int cmd_clone(int argc,\n \n \ttransport_set_option(transport, TRANS_OPT_KEEP, \"yes\");\n \n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  !!option_branch, \"--branch\");\n+\tdie_for_incompatible_opt2(!!option_rev, \"--revision\",\n+\t\t\t\t  option_mirror, \"--mirror\");\n+\n \tif (reject_shallow)\n \t\ttransport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, \"1\");\n \tif (option_depth)\n@@ -1378,7 +1389,14 @@ int cmd_clone(int argc,\n \tif (transport->smart_options && !deepen && !filter_options.choice)\n \t\ttransport->smart_options->check_self_contained_and_connected = 1;\n \n-\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\tif (option_rev) {\n+\t\toption_tags = 0;\n+\t\toption_single_branch = 0;\n+\t\topts.wants_head = 0;\n+\t\topts.detach = 1;\n+\n+\t\trefspec_append(&remote->fetch, option_rev);\n+\t}\n \n \tif (option_tags || option_branch)\n \t\t/*\n@@ -1393,6 +1411,17 @@ int cmd_clone(int argc,\n \t\texpand_ref_prefix(&transport_ls_refs_options.ref_prefixes,\n \t\t\t\t  option_branch);\n \n+\t/*\n+\t * As part of transport_get_remote_refs() the server tells us the hash\n+\t * algorithm, which we require to initialize the repo. But calling that\n+\t * function without any ref prefix, will cause the server to announce\n+\t * all known refs. If the argument passed to --revision was a hex oid,\n+\t * ref_prefixes will be empty so we fall back to asking about HEAD to\n+\t * reduce traffic from the server.\n+\t */\n+\tif (opts.wants_head || transport_ls_refs_options.ref_prefixes.nr == 0)\n+\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n \t/*\n@@ -1501,6 +1530,11 @@ int cmd_clone(int argc,\n \t\tif (!our_head_points_at)\n \t\t\tdie(_(\"Remote branch %s not found in upstream %s\"),\n \t\t\t    option_branch, remote_name);\n+\t} else if (option_rev) {\n+\t\tour_head_points_at = mapped_refs;\n+\t\tif (!our_head_points_at)\n+\t\t\tdie(_(\"Remote revision %s not found in upstream %s\"),\n+\t\t\t    option_rev, remote_name);\n \t} else if (remote_head_points_at) {\n \t\tour_head_points_at = remote_head_points_at;\n \t} else if (remote_head) {\n@@ -1539,8 +1573,9 @@ int cmd_clone(int argc,\n \t\tfree(to_free);\n \t}\n \n-\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n-\t\t\tremote_head_points_at, &branch_top);\n+\tif (!option_rev)\n+\t\twrite_refspec_config(src_ref_prefix, our_head_points_at,\n+\t\t\t\t     remote_head_points_at, &branch_top);\n \n \tif (filter_options.choice)\n \t\tpartial_clone_register(remote_name, &filter_options);\n@@ -1556,7 +1591,7 @@ int cmd_clone(int argc,\n \t\t\t   branch_top.buf, reflog_msg.buf, transport,\n \t\t\t   !is_local);\n \n-\tupdate_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n+\tupdate_head(&opts, our_head_points_at, remote_head, unborn_head, reflog_msg.buf);\n \n \t/*\n \t * We want to show progress for recursive submodule clones iff\ndiff --git a/t/meson.build b/t/meson.build\nindex 35f25ca4a1d960564190288e9456620a46ccc80a..b5f917926b61de379b6cef45e5f750912422a7d1 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -721,6 +721,7 @@ integration_tests = [\n   't5617-clone-submodules-remote.sh',\n   't5618-alternate-refs.sh',\n   't5619-clone-local-ambiguous-transport.sh',\n+  't5621-clone-revision.sh',\n   't5700-protocol-v1.sh',\n   't5701-git-serve.sh',\n   't5702-protocol-v2.sh',\ndiff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n--- /dev/null\n+++ b/t/t5621-clone-revision.sh\n@@ -0,0 +1,123 @@\n+#!/bin/sh\n+\n+test_description='tests for git clone --revision'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit --no-tag \"initial commit\" README \"Hello\" &&\n+\ttest_commit --annotate \"second commit\" README \"Hello world\" v1.0 &&\n+\ttest_commit --no-tag \"third commit\" README \"Hello world!\" &&\n+\tgit switch -c feature v1.0 &&\n+\ttest_commit --no-tag \"feature commit\" README \"Hello world!\" &&\n+\tgit switch main\n+'\n+\n+test_expect_success 'clone with --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --depth and --revision being a branch' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --no-local --depth=1 --revision=refs/heads/feature . dst &&\n+\tgit rev-parse refs/heads/feature >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch &&\n+\tgit -C dst rev-list HEAD >actual &&\n+\ttest_line_count = 1 actual\n+'\n+\n+test_expect_success 'clone with --revision being a tag' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/tags/v1.0 . dst &&\n+\tgit rev-parse refs/tags/v1.0^{} >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being HEAD' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=HEAD . dst &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature) &&\n+\tgit clone --revision=$oid . dst &&\n+\techo $oid >expect &&\n+\tgit -C dst rev-parse HEAD >actual &&\n+\ttest_must_fail git -C dst symbolic-ref -q HEAD >/dev/null &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision and --bare' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\tgit clone --revision=refs/heads/main --bare . dst &&\n+\toid=$(git rev-parse refs/heads/main) &&\n+\tgit -C dst cat-file -t $oid >actual &&\n+\techo \"commit\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C dst for-each-ref refs >expect &&\n+\ttest_must_be_empty expect &&\n+\ttest_must_fail git -C dst config remote.origin.fetch\n+'\n+\n+test_expect_success 'clone with --revision being a short raw commit hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse --short refs/heads/feature) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"fatal: Remote revision $oid not found in upstream origin\" err\n+'\n+\n+test_expect_success 'clone with --revision being a tree hash' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\toid=$(git rev-parse refs/heads/feature^{tree}) &&\n+\ttest_must_fail git clone --revision=$oid . dst 2>err &&\n+\ttest_grep \"error: object $oid is a tree, not a commit\" err\n+'\n+\n+test_expect_success 'clone with --revision being the parent of a ref fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main^ . dst\n+'\n+\n+test_expect_success 'clone with --revision and --branch fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --branch=main . dst\n+'\n+\n+test_expect_success 'clone with --revision and --mirror fails' '\n+\ttest_when_finished \"rm -rf dst\" &&\n+\ttest_must_fail git clone --revision=refs/heads/main --mirror . dst\n+'\n+\n+test_done\n\n-- \n2.48.1.447.gc0086e9015\n\n"},{"id":"511985","messageId":"Z6RvD4FtfXIk4Pa3@pks.im","threadId":"62566","inReplyTo":"20250206-toon-clone-refs-v7-7-4622b7392202@iotcl.com","subject":"Re: [PATCH v7 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-06T08:13:03Z","receivedAt":"2025-02-06T08:13:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 06, 2025 at 07:33:35AM +0100, Toon Claes wrote:\n> diff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\n> new file mode 100755\n> index 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n> --- /dev/null\n> +++ b/t/t5621-clone-revision.sh\n> @@ -0,0 +1,123 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git clone --revision'\n> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> +\n> +TEST_PASSES_SANITIZE_LEAK=true\n\nOne last nit: this line is not needed anymore, as tests are required to\npass with the leak sanitizer by default now. Other than that this series\nlooks good to me, and this change alone does not warrant a reroll from\nmy point of view.\n\nThanks!\n\nPatrick\n"},{"id":"512014","messageId":"xmqq4j16kefu.fsf@gitster.g","threadId":"62566","inReplyTo":"Z6RvD4FtfXIk4Pa3@pks.im","subject":"Re: [PATCH v7 7/7] builtin/clone: teach git-clone(1) the --revision= option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-06T20:26:13Z","receivedAt":"2025-02-06T20:26:17Z","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 Thu, Feb 06, 2025 at 07:33:35AM +0100, Toon Claes wrote:\n>> diff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh\n>> new file mode 100755\n>> index 0000000000000000000000000000000000000000..d4889a954e6300e0e327ebe7dfcf73569d966829\n>> --- /dev/null\n>> +++ b/t/t5621-clone-revision.sh\n>> @@ -0,0 +1,123 @@\n>> +#!/bin/sh\n>> +\n>> +test_description='tests for git clone --revision'\n>> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>> +\n>> +TEST_PASSES_SANITIZE_LEAK=true\n>\n> One last nit: this line is not needed anymore, as tests are required to\n> pass with the leak sanitizer by default now. Other than that this series\n> looks good to me, and this change alone does not warrant a reroll from\n> my point of view.\n>\n> Thanks!\n\nI'll touch it up while queuing, and mark the topic for 'next', then.\n\nThanks.\n"}]}