{"thread":{"id":"65366","subject":"[PATCH 0/3] Add support for per-remote and per-namespace SSH options","startedAt":"2026-03-26T23:37:49Z","lastAt":"2026-03-29T14:34:11Z","messageCount":24,"participants":["Wesley Schwengle","Johannes Sixt","Wesley","Junio C Hamano","Jeff King","brian m. carlson","Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"540153","messageId":"20260326233739.2911354-1-wesleys@opperschaap.net","threadId":"65366","inReplyTo":null,"subject":"[PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Wesley Schwengle","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-26T23:37:35Z","receivedAt":"2026-03-26T23:37:49Z","isPatch":true,"body":"With this changeset applied git is now aware of `sshIdentityFiles' and\n`sshOpts'. This allows users to have multiple accounts on the same forges.\nA common problem within the developer community. This problem is often\nsolved by hacking in one's `.ssh/config' and changing hostname URIs to\nensure the correct key is being used.\n\nFor years I had zsh wrapper script that was used as the `core.sshCommand' and\nis a reference implementation of this change.\n\nIn order of importance:\n\nConfiguration on the remotes itself. This is easy, straight forward and\nshould allow people to get it to work quickly:\n\n* `remote.*.sshIdentityFile' and `remote.*.sshOpts'\n\nConfiguration set on owner/path style. This is to support `includeIf`\nconfiguration management. For example, a git-forge that host both\nemployer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and\n`git@gitlab.com/corp/git.git' would have something configured as:\n\n* `core.sshIdentityFile.*', eg\n\n    [core \"sshIdentityFile\"]\n      waterkip = ~/.ssh/id_ed25519_me\n      corp     = ~/.ssh/id_ed25519_corporate\n\nAnd finally, a global override for everything:\n\n* `core.sshIdentityFile' and `core.sshOpts'\n\nI stayed within the `core' namespace, mainly because `core.sshCommand'. I'm\nhappy to move it to `ssh' or something similar. It would perhaps make\n`ssh.*.sshIdentifyFile' more structured, because now that's split between two\ncore subsections.\n\nThe following assumptions have been made to make it safe and sound for\nusers. When an `sshIdentityFile' is used and no `sshOpts' are configured git\nwill inject `-F /dev/null' to prevent cycling over all sshIdentityFiles\na user has in their `.ssh/config'. When a user configures `sshOpts', these\ntake precedence and a user itself is responsible for setting\n`-F /dev/null'.\n\nSeparate push/pull URIs are not supported by the feature. The biggest problem\nwith this is that I don't know how to properly configure them with the\nnamespace constraints. `remote.*.xyz' is as deep as git can go and a push/pull\nwould require additional configuration. I filed it under edge-case.\n\nThere are two new structs introduced: `ssh_options' and `cnx_context'.\nThey are there to limit the amount of argument passing down the wire. And this\nis especially true for `ssh_options' because it keeps `push_ssh_options' dumb.\n\nWesley Schwengle (3):\n  connect: Rename name to command in connect_git()\n  connect: Add transport->remote->name to git_connect()\n  connect: Add support for per-remote and per-namespace SSH options\n\n Documentation/config/core.adoc   |  22 ++++\n Documentation/config/remote.adoc |   9 ++\n builtin/fetch-pack.c             |   2 +-\n builtin/send-pack.c              |   2 +-\n connect.c                        | 144 ++++++++++++++++++++--\n connect.h                        |   2 +-\n t/t57xx-ssh-options-config.sh    | 198 +++++++++++++++++++++++++++++++\n transport.c                      |   9 +-\n 8 files changed, 375 insertions(+), 13 deletions(-)\n create mode 100755 t/t57xx-ssh-options-config.sh\n\n-- \n2.53.0.722.g8e572876c5\n\n"},{"id":"540154","messageId":"20260326233739.2911354-2-wesleys@opperschaap.net","threadId":"65366","inReplyTo":"20260326233739.2911354-1-wesleys@opperschaap.net","subject":"[PATCH 1/3] connect: Rename name to command in connect_git()","fromName":"Wesley Schwengle","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-26T23:37:36Z","receivedAt":"2026-03-26T23:37:57Z","isPatch":true,"body":"connect_git has `char *name' in its signature and it caught me a little\noffguard. I initially thought it was the remote name. But when you look\ncloser at the various call sites it is actually a command that is send\nover the wire, eg . `git-receive-pack'. Change the naming makes it\neasier to read the code and understand its intention.\n\nSigned-off-by: Wesley Schwengle <wesleys@opperschaap.net>\n---\n connect.c   | 4 ++--\n connect.h   | 2 +-\n transport.c | 4 ++--\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex a02583a102..29af453b41 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1420,35 +1420,35 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n  * does not need fork(2), or a struct child_process object if it does.  Once\n  * done, finish the connection with finish_connect() with the value returned\n  * from this function (it is safe to call finish_connect() with NULL to\n  * support the former case).\n  *\n  * If it returns, the connect is successful; it just dies on errors (this\n  * will hopefully be changed in a libification effort, to return NULL when\n  * the connection failed).\n  */\n struct child_process *git_connect(int fd[2], const char *url,\n-\t\t\t\t  const char *name,\n+\t\t\t\t  const char *command,\n \t\t\t\t  const char *prog, int flags)\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n \tenum protocol protocol;\n \tenum protocol_version version = get_protocol_version_config();\n \n \t/*\n \t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n \t * to perform any operation that doesn't involve upload-pack (i.e., a\n \t * fetch, ls-remote, etc), then fallback to v0 since we don't know how\n \t * to do anything else (like push or remote archive) via v2.\n \t */\n-\tif (version == protocol_v2 && strcmp(\"git-upload-pack\", name))\n+\tif (version == protocol_v2 && strcmp(\"git-upload-pack\", command))\n \t\tversion = protocol_v0;\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n \t */\n \tsignal(SIGCHLD, SIG_DFL);\n \n \tprotocol = parse_connect_url(url, &hostandport, &path);\n \tif ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) {\n \t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\ndiff --git a/connect.h b/connect.h\nindex 1645126c17..f993626473 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -1,20 +1,20 @@\n #ifndef CONNECT_H\n #define CONNECT_H\n \n #include \"protocol.h\"\n \n #define CONNECT_VERBOSE       (1u << 0)\n #define CONNECT_DIAG_URL      (1u << 1)\n #define CONNECT_IPV4          (1u << 2)\n #define CONNECT_IPV6          (1u << 3)\n-struct child_process *git_connect(int fd[2], const char *url, const char *name, const char *prog, int flags);\n+struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, int flags);\n int finish_connect(struct child_process *conn);\n int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\n int parse_feature_request(const char *features, const char *feature);\n const char *server_feature_value(const char *feature, size_t *len_ret);\n int url_is_local_not_ssh(const char *url);\n \n struct packet_reader;\n enum protocol_version discover_version(struct packet_reader *reader);\n \ndiff --git a/transport.c b/transport.c\nindex cb1befba8c..27a99190c0 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -949,26 +949,26 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re\n \n \tclose(data->fd[1]);\n \tclose(data->fd[0]);\n \tret |= finish_connect(data->conn);\n \tdata->conn = NULL;\n \tdata->finished_handshake = 0;\n \n \treturn ret;\n }\n \n-static int connect_git(struct transport *transport, const char *name,\n+static int connect_git(struct transport *transport, const char *command,\n \t\t       const char *executable, int fd[2])\n {\n \tstruct git_transport_data *data = transport->data;\n \tdata->conn = git_connect(data->fd, transport->url,\n-\t\t\t\t name, executable, 0);\n+\t\t\t\t command, executable, 0);\n \tfd[0] = data->fd[0];\n \tfd[1] = data->fd[1];\n \treturn 0;\n }\n \n static int disconnect_git(struct transport *transport)\n {\n \tstruct git_transport_data *data = transport->data;\n \tif (data->conn) {\n \t\tif (data->finished_handshake && !transport->stateless_rpc)\n-- \n2.53.0.722.g8e572876c5\n\n"},{"id":"540155","messageId":"20260326233739.2911354-3-wesleys@opperschaap.net","threadId":"65366","inReplyTo":"20260326233739.2911354-1-wesleys@opperschaap.net","subject":"[PATCH 2/3] connect: Add transport->remote->name to git_connect()","fromName":"Wesley Schwengle","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-26T23:37:37Z","receivedAt":"2026-03-26T23:38:00Z","isPatch":true,"body":"To support `remote.$name.sshIdentityFile', and `remote.$name.sshOpts' for\nconnecting to various remotes I need to pass around the remote down to\ngit_connect. This commit introduces the `remote_name' and sprinkles all\ncall sites to pass `NULL'. This is a non-breaking forward change\n\nSigned-off-by: Wesley Schwengle <wesleys@opperschaap.net>\n---\n builtin/fetch-pack.c | 2 +-\n builtin/send-pack.c  | 2 +-\n connect.c            | 5 +++--\n connect.h            | 2 +-\n transport.c          | 7 ++++++-\n 5 files changed, 12 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex d9e42bad58..f422acce06 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -217,21 +217,21 @@ int cmd_fetch_pack(int argc,\n \n \tif (args.stateless_rpc) {\n \t\tconn = NULL;\n \t\tfd[0] = 0;\n \t\tfd[1] = 1;\n \t} else {\n \t\tint flags = args.verbose ? CONNECT_VERBOSE : 0;\n \t\tif (args.diag_url)\n \t\t\tflags |= CONNECT_DIAG_URL;\n \t\tconn = git_connect(fd, dest, \"git-upload-pack\",\n-\t\t\t\t   args.uploadpack, flags);\n+\t\t\t\t   args.uploadpack, NULL, flags);\n \t\tif (!conn)\n \t\t\treturn args.diag_url ? 0 : 1;\n \t}\n \n \tpacket_reader_init(&reader, fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_GENTLE_ON_EOF |\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tversion = discover_version(&reader);\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8b81c8a848..65efa91208 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -266,21 +266,21 @@ int cmd_send_pack(int argc,\n \n \tif (progress == -1)\n \t\tprogress = !args.quiet && isatty(2);\n \targs.progress = progress;\n \n \tif (args.stateless_rpc) {\n \t\tconn = NULL;\n \t\tfd[0] = 0;\n \t\tfd[1] = 1;\n \t} else {\n-\t\tconn = git_connect(fd, dest, \"git-receive-pack\", receivepack,\n+\t\tconn = git_connect(fd, dest, \"git-receive-pack\", receivepack, NULL,\n \t\t\targs.verbose ? CONNECT_VERBOSE : 0);\n \t}\n \n \tpacket_reader_init(&reader, fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_GENTLE_ON_EOF |\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tswitch (discover_version(&reader)) {\n \tcase protocol_v2:\ndiff --git a/connect.c b/connect.c\nindex 29af453b41..5749ddec9b 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1420,22 +1420,22 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n  * does not need fork(2), or a struct child_process object if it does.  Once\n  * done, finish the connection with finish_connect() with the value returned\n  * from this function (it is safe to call finish_connect() with NULL to\n  * support the former case).\n  *\n  * If it returns, the connect is successful; it just dies on errors (this\n  * will hopefully be changed in a libification effort, to return NULL when\n  * the connection failed).\n  */\n struct child_process *git_connect(int fd[2], const char *url,\n-\t\t\t\t  const char *command,\n-\t\t\t\t  const char *prog, int flags)\n+\t\t\t\t  const char *command, const char *prog,\n+\t\t\t\t  const char *remote_name, int flags)\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n \tenum protocol protocol;\n \tenum protocol_version version = get_protocol_version_config();\n \n \t/*\n \t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n \t * to perform any operation that doesn't involve upload-pack (i.e., a\n \t * fetch, ls-remote, etc), then fallback to v0 since we don't know how\n@@ -1487,20 +1487,21 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\t\tif (!port)\n \t\t\t\tport = get_port(ssh_host);\n \n \t\t\tif (flags & CONNECT_DIAG_URL) {\n \t\t\t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n \t\t\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n \t\t\t\tprintf(\"Diag: userandhost=%s\\n\", ssh_host ? ssh_host : \"NULL\");\n \t\t\t\tprintf(\"Diag: port=%s\\n\", port ? port : \"NONE\");\n \t\t\t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n+\t\t\t\tprintf(\"Diag: remote=%s\\n\", remote_name ? remote_name : \"NULL\");\n \n \t\t\t\tfree(hostandport);\n \t\t\t\tfree(path);\n \t\t\t\tchild_process_clear(conn);\n \t\t\t\tfree(conn);\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n \t\t\tconn->trace2_child_class = \"transport/ssh\";\n \t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\ndiff --git a/connect.h b/connect.h\nindex f993626473..ff54061e81 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -1,20 +1,20 @@\n #ifndef CONNECT_H\n #define CONNECT_H\n \n #include \"protocol.h\"\n \n #define CONNECT_VERBOSE       (1u << 0)\n #define CONNECT_DIAG_URL      (1u << 1)\n #define CONNECT_IPV4          (1u << 2)\n #define CONNECT_IPV6          (1u << 3)\n-struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, int flags);\n+struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, const char *remote_name, int flags);\n int finish_connect(struct child_process *conn);\n int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\n int parse_feature_request(const char *features, const char *feature);\n const char *server_feature_value(const char *feature, size_t *len_ret);\n int url_is_local_not_ssh(const char *url);\n \n struct packet_reader;\n enum protocol_version discover_version(struct packet_reader *reader);\n \ndiff --git a/transport.c b/transport.c\nindex 27a99190c0..b9dcbf8d9e 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -289,37 +289,39 @@ static int set_git_option(struct git_transport_options *opts,\n \t\topts->reject_shallow = !!value;\n \t\treturn 0;\n \t}\n \treturn 1;\n }\n \n static int connect_setup(struct transport *transport, int for_push)\n {\n \tstruct git_transport_data *data = transport->data;\n \tint flags = transport->verbose > 0 ? CONNECT_VERBOSE : 0;\n+\tconst char *remote_name = transport->remote->name;\n \n \tif (data->conn)\n \t\treturn 0;\n \n \tswitch (transport->family) {\n \tcase TRANSPORT_FAMILY_ALL: break;\n \tcase TRANSPORT_FAMILY_IPV4: flags |= CONNECT_IPV4; break;\n \tcase TRANSPORT_FAMILY_IPV6: flags |= CONNECT_IPV6; break;\n \t}\n \n \tdata->conn = git_connect(data->fd, transport->url,\n \t\t\t\t for_push ?\n \t\t\t\t\t\"git-receive-pack\" :\n \t\t\t\t\t\"git-upload-pack\",\n \t\t\t\t for_push ?\n \t\t\t\t\tdata->options.receivepack :\n \t\t\t\t\tdata->options.uploadpack,\n+\t\t\t\t remote_name,\n \t\t\t\t flags);\n \n \treturn 0;\n }\n \n static void die_if_server_options(struct transport *transport)\n {\n \tif (!transport->server_options || !transport->server_options->nr)\n \t\treturn;\n \tadvise(_(\"see protocol.version in 'git help config' for more details\"));\n@@ -953,22 +955,25 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re\n \tdata->conn = NULL;\n \tdata->finished_handshake = 0;\n \n \treturn ret;\n }\n \n static int connect_git(struct transport *transport, const char *command,\n \t\t       const char *executable, int fd[2])\n {\n \tstruct git_transport_data *data = transport->data;\n+\tconst char *remote_name = transport->remote->name;\n+\n \tdata->conn = git_connect(data->fd, transport->url,\n-\t\t\t\t command, executable, 0);\n+\t\t\t\t command, executable, remote_name,\n+\t\t\t\t 0);\n \tfd[0] = data->fd[0];\n \tfd[1] = data->fd[1];\n \treturn 0;\n }\n \n static int disconnect_git(struct transport *transport)\n {\n \tstruct git_transport_data *data = transport->data;\n \tif (data->conn) {\n \t\tif (data->finished_handshake && !transport->stateless_rpc)\n-- \n2.53.0.722.g8e572876c5\n\n"},{"id":"540156","messageId":"20260326233739.2911354-4-wesleys@opperschaap.net","threadId":"65366","inReplyTo":"20260326233739.2911354-1-wesleys@opperschaap.net","subject":"[PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options","fromName":"Wesley Schwengle","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-26T23:37:38Z","receivedAt":"2026-03-26T23:38:11Z","isPatch":true,"body":"Git relied on external SSH configuration (e.g. `~/.ssh/config')zR\nor wrapper scripts to select identity files and additional SSH options.\n\nThis commit adds support for configuring SSH options directly in\ngit config. Making it easier for users to select the correct identity at\ntheir respective forges.\n\nThe following configuration is supported, in order of precedence:\n\n  1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts'\n\n  2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>'\n\n     Where <owner> is derived from the repository path. Nested groups\n     aren't supported: git@host:owner/repo.git becomes \"owner\",\n     git@host:owner/group/repo.git also becomes \"owner\".\n\n  3. `core.sshIdentityFile' and `core.sshOpts'\n\nWhen `sshIdentityFile' is configured without `sshOpts', we inject\n`-F /dev/null' to prevent selecting additional identities from\n`~/.ssh/config'. If `sshOpts' are provided, it is used as-is and the\nuser is responsible for specifying `-F /dev/null' if desired.\n\nThis allows selecting SSH identities and options without relying on\nhost aliases or wrapper scripts.\n\nImplementation details:\n\n  * Introduce a connection context (cnx_context) to carry the remote\n    name and repository owner.\n  * Introduce ssh_options to encapsulate resolved SSH configuration.\n\nLimitations:\n\n  * Separate push/pull URLs are not supported.\n  * OpenSSH is the only supported ssh implemenation.\n\nSigned-off-by: Wesley Schwengle <wesleys@opperschaap.net>\n---\n Documentation/config/core.adoc   |  22 ++++\n Documentation/config/remote.adoc |   9 ++\n connect.c                        | 137 ++++++++++++++++++++-\n t/t57xx-ssh-options-config.sh    | 198 +++++++++++++++++++++++++++++++\n 4 files changed, 361 insertions(+), 5 deletions(-)\n create mode 100755 t/t57xx-ssh-options-config.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex a0ebf03e2e..6a221bdf3b 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -263,20 +263,42 @@ specify that no proxy be used for a given domain pattern.\n This is useful for excluding servers inside a firewall from\n proxy use, while defaulting to a common proxy for external domains.\n \n core.sshCommand::\n \tIf this variable is set, `git fetch` and `git push` will\n \tuse the specified command instead of `ssh` when they need to\n \tconnect to a remote system. The command is in the same form as\n \tthe `GIT_SSH_COMMAND` environment variable and is overridden\n \twhen the environment variable is set.\n \n+core.sshIdentityFile::\n+\tDefault SSH identity file to use for SSH transports. When an\n+\t`sshIdentityFile` is used, git adds `-o IdentitiesOnly=yes` to the ssh\n+\toptions by default. This feature currently only supports OpenSSH.\n+\n+core.sshOpts::\n+\tDefault additional options to pass to the SSH command.\n+\tWhen `sshIdentityFile` is configured without `sshOpts`, git adds `-F\n+\t/dev/null` to the SSH invocation.  When `sshOpts` is configured, it is\n+\tused as-is. This feature currently only supports OpenSSH.\n+\n+core.sshIdentityFile.<owner>::\n+\tSSH identity file to use for repositories whose path begins with\n+\t`<owner>`. For example, `git@host:owner/repo.git` uses `owner`.\n+\tOverrides `core.sshIdentityFile` when `core.sshIdentityFile.<owner>`\n+\tequals the owner.\n+\n+core.sshOpts.<owner>::\n+\tDefault additional SSH options for repositories whose path begins\n+\twith `<owner>`.\n+\tOverrides `core.sshOpts` when `core.sshOpts.<owner>` equals the owner.\n+\n core.ignoreStat::\n \tIf true, Git will avoid using lstat() calls to detect if files have\n \tchanged by setting the \"assume-unchanged\" bit for those tracked files\n \twhich it has updated identically in both the index and working tree.\n +\n When files are modified outside of Git, the user will need to stage\n the modified files explicitly (e.g. see 'Examples' section in\n linkgit:git-update-index[1]).\n Git will not normally detect changes to those files.\n +\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 91e46f66f5..b40e30eb41 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -113,10 +113,19 @@ remote.<name>.followRemoteHEAD::\n \tThe default value is \"create\", which will create `remotes/<name>/HEAD`\n \tif it exists on the remote, but not locally; this will not touch an\n \talready existing local reference. Setting it to \"warn\" will print\n \ta message if the remote has a different value than the local one;\n \tin case there is no local reference, it behaves like \"create\".\n \tA variant on \"warn\" is \"warn-if-not-$branch\", which behaves like\n \t\"warn\", but if `HEAD` on the remote is `$branch` it will be silent.\n \tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n \tthe value on the remote.  Finally, setting it to \"never\" will never\n \tchange or create the local reference.\n+\n+remote.<name>.sshIdentityFile::\n+\tPath to the SSH identity file to use for this remote when connecting\n+\tover SSH. Overrides `core.sshIdentityFile` and\n+\t`core.sshIdentityFile.<owner>`.\n+\n+remote.<name>.sshOpts::\n+\tAdditional options to pass to the SSH command for this remote.\n+\tOverrides `core.sshOpts` and `core.sshOpts.<owner>`.\ndiff --git a/connect.c b/connect.c\nindex 5749ddec9b..d185c1679a 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -21,20 +21,30 @@\n #include \"version.h\"\n #include \"protocol.h\"\n #include \"alias.h\"\n #include \"bundle-uri.h\"\n #include \"promisor-remote.h\"\n \n static char *server_capabilities_v1;\n static struct strvec server_capabilities_v2 = STRVEC_INIT;\n static const char *next_server_feature_value(const char *feature, size_t *len, size_t *offset);\n \n+struct cnx_context {\n+\tchar *owner;\n+\tconst char *remote_name;\n+};\n+\n+struct ssh_options {\n+\tconst char *identity_file;\n+\tstruct strvec ssh_opts;\n+};\n+\n static int check_ref(const char *name, unsigned int flags)\n {\n \tif (!flags)\n \t\treturn 1;\n \n \tif (!skip_prefix(name, \"refs/\", &name))\n \t\treturn 0;\n \n \t/* REF_NORMAL means that we don't want the magic fake tag refs */\n \tif ((flags & REF_NORMAL) && check_refname_format(name,\n@@ -1295,34 +1305,140 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t    version, '\\0');\n \t}\n \n \tpacket_write(fd[1], request.buf, request.len);\n \n \tfree(target_host);\n \tstrbuf_release(&request);\n \treturn conn;\n }\n \n+static const char *get_ssh_config_values(struct cnx_context context,\n+\t\tconst char *lookup) {\n+\tstruct strbuf key = STRBUF_INIT;\n+\tconst char *value = NULL;\n+\n+\tif (context.remote_name) {\n+\t\tstrbuf_addf(&key, \"remote.%s.%s\", context.remote_name, lookup);\n+\t\tif (!repo_config_get_string_tmp(the_repository, key.buf, &value)) {\n+\t\t\tstrbuf_release(&key);\n+\t\t\treturn value;\n+\t\t}\n+\t\tstrbuf_reset(&key);\n+\t}\n+\tif (context.owner) {\n+\t\tstrbuf_addf(&key, \"core.%s.%s\", lookup, context.owner);\n+\t\tif (!repo_config_get_string_tmp(the_repository, key.buf, &value)) {\n+\t\t\tstrbuf_release(&key);\n+\t\t\treturn value;\n+\t\t}\n+\t\tstrbuf_reset(&key);\n+\t}\n+\tstrbuf_addf(&key, \"core.%s\", lookup);\n+\tif (!repo_config_get_string_tmp(the_repository, key.buf, &value)) {\n+\t\tstrbuf_release(&key);\n+\t\treturn value;\n+\t}\n+\n+\tstrbuf_release(&key);\n+\treturn NULL;\n+}\n+\n+/*\n+ * Returns the first path component of `path`, which the caller must free().\n+ * Returns NULL if `path` is NULL or has no '/' separator.\n+ */\n+static char *repo_namespace(const char *path)\n+{\n+\tconst char *slash;\n+\n+\tif (!path)\n+\t\treturn NULL;\n+\n+\twhile (*path == '/')\n+\t\tpath++;\n+\n+\tslash = strchr(path, '/');\n+\tif (!slash)\n+\t\treturn NULL;\n+\n+\treturn xstrndup(path, slash - path);\n+}\n+\n+static struct ssh_options *get_ssh_options(struct cnx_context context)\n+{\n+\tstruct ssh_options *opts = xcalloc(1, sizeof(*opts));\n+\tconst char *sshopts;\n+\tstrvec_init(&opts->ssh_opts);\n+\n+\topts->identity_file = get_ssh_config_values(context,\n+\t\t\t\t\t\t       \"sshIdentityFile\");\n+\n+\tsshopts = get_ssh_config_values(context, \"sshOpts\");\n+\n+\tif (sshopts) {\n+\t\tconst char **argv = NULL;\n+\t\tchar *cmdline = xstrdup(sshopts);\n+\t\tint argc = split_cmdline(cmdline, &argv);\n+\t\tint i;\n+\n+\t\tif (argc < 0)\n+\t\t\tdie(_(\"bad sshOpts value: '%s'\"), sshopts);\n+\n+\t\tfor (i = 0; i < argc; i++)\n+\t\t\tstrvec_push(&opts->ssh_opts, argv[i]);\n+\n+\t\tfree((void *)argv);\n+\t\tfree(cmdline);\n+\t}\n+\n+\treturn opts;\n+}\n+\n+static void clear_ssh_options(struct ssh_options *opts)\n+{\n+\tstrvec_clear(&opts->ssh_opts);\n+\tfree(opts);\n+}\n+\n /*\n  * Append the appropriate environment variables to `env` and options to\n  * `args` for running ssh in Git's SSH-tunneled transport.\n  */\n static void push_ssh_options(struct strvec *args, struct strvec *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     enum protocol_version version,\n+\t\t\t     struct ssh_options *ssh_options, int flags)\n {\n \tif (variant == VARIANT_SSH &&\n \t    version > 0) {\n \t\tstrvec_push(args, \"-o\");\n \t\tstrvec_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n \t\tstrvec_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n \t\t\t     version);\n+\n+\t}\n+\tif (variant == VARIANT_SSH) {\n+\t\tif (ssh_options->identity_file) {\n+\t\t\tstrvec_push(args, \"-i\");\n+\t\t\tstrvec_push(args, ssh_options->identity_file);\n+\t\t\tstrvec_push(args, \"-o\");\n+\t\t\tstrvec_push(args, \"IdentitiesOnly=yes\");\n+\t\t}\n+\n+\t\tif (ssh_options->identity_file && !ssh_options->ssh_opts.nr) {\n+\t\t\tstrvec_push(args, \"-F\");\n+\t\t\tstrvec_push(args, \"/dev/null\");\n+\t\t}\n+\t\tif (ssh_options->ssh_opts.nr > 0) {\n+\t\t\tstrvec_pushv(args, ssh_options->ssh_opts.v);\n+\t\t}\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n \t\tswitch (variant) {\n \t\tcase VARIANT_AUTO:\n \t\t\tBUG(\"VARIANT_AUTO passed to push_ssh_options\");\n \t\tcase VARIANT_SIMPLE:\n \t\t\tdie(_(\"ssh variant 'simple' does not support -4\"));\n \t\tcase VARIANT_SSH:\n \t\tcase VARIANT_PLINK:\n@@ -1362,21 +1478,21 @@ static void push_ssh_options(struct strvec *args, struct strvec *env,\n \t\t\tstrvec_push(args, \"-P\");\n \t\t}\n \n \t\tstrvec_push(args, port);\n \t}\n }\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \t\t\t  const char *port, enum protocol_version version,\n-\t\t\t  int flags)\n+\t\t\t  struct ssh_options *ssh_options, int flags)\n {\n \tconst char *ssh;\n \tenum ssh_variant variant;\n \n \tif (looks_like_command_line_option(ssh_host))\n \t\tdie(_(\"strange hostname '%s' blocked\"), ssh_host);\n \n \tssh = get_ssh_command();\n \tif (ssh) {\n \t\tvariant = determine_ssh_variant(ssh, 1);\n@@ -1396,29 +1512,29 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \tif (variant == VARIANT_AUTO) {\n \t\tstruct child_process detect = CHILD_PROCESS_INIT;\n \n \t\tdetect.use_shell = conn->use_shell;\n \t\tdetect.no_stdin = detect.no_stdout = detect.no_stderr = 1;\n \n \t\tstrvec_push(&detect.args, ssh);\n \t\tstrvec_push(&detect.args, \"-G\");\n \t\tpush_ssh_options(&detect.args, &detect.env,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\t\t\t VARIANT_SSH, port, version, ssh_options, flags);\n \t\tstrvec_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \tstrvec_push(&conn->args, ssh);\n \tpush_ssh_options(&conn->args, &conn->env, variant, port, version,\n-\t\t\t flags);\n+\t\t\t ssh_options, flags);\n \tstrvec_push(&conn->args, ssh_host);\n }\n \n /*\n  * This returns the dummy child_process `no_fork` if the transport protocol\n  * does not need fork(2), or a struct child_process object if it does.  Once\n  * done, finish the connection with finish_connect() with the value returned\n  * from this function (it is safe to call finish_connect() with NULL to\n  * support the former case).\n  *\n@@ -1475,20 +1591,22 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\t/* remove repo-local variables from the environment */\n \t\tfor (var = local_repo_env; *var; var++)\n \t\t\tstrvec_push(&conn->env, *var);\n \n \t\tconn->use_shell = 1;\n \t\tconn->in = conn->out = -1;\n \t\tif (protocol == PROTO_SSH) {\n \t\t\tchar *ssh_host = hostandport;\n \t\t\tconst char *port = NULL;\n+\t\t\tstruct ssh_options *ssh_options;\n+\t\t\tstruct cnx_context context;\n \t\t\ttransport_check_allowed(\"ssh\");\n \t\t\tget_host_and_port(&ssh_host, &port);\n \n \t\t\tif (!port)\n \t\t\t\tport = get_port(ssh_host);\n \n \t\t\tif (flags & CONNECT_DIAG_URL) {\n \t\t\t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n \t\t\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n \t\t\t\tprintf(\"Diag: userandhost=%s\\n\", ssh_host ? ssh_host : \"NULL\");\n@@ -1496,22 +1614,31 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\t\t\tprintf(\"Diag: remote=%s\\n\", remote_name ? remote_name : \"NULL\");\n \n \t\t\t\tfree(hostandport);\n \t\t\t\tfree(path);\n \t\t\t\tchild_process_clear(conn);\n \t\t\t\tfree(conn);\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n+\n+\t\t\tcontext.owner = repo_namespace(path);\n+\t\t\tcontext.remote_name = remote_name;\n+\t\t\tssh_options = get_ssh_options(context);\n+\n \t\t\tconn->trace2_child_class = \"transport/ssh\";\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, version,\n+\t\t\t\t      ssh_options, flags);\n+\n+\t\t\tclear_ssh_options(ssh_options);\n+\t\t\tfree(context.owner);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n \t\t\tconn->trace2_child_class = \"transport/file\";\n \t\t\tif (version > 0) {\n \t\t\t\tstrvec_pushf(&conn->env,\n \t\t\t\t\t     GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n \t\t\t\t\t     version);\n \t\t\t}\n \t\t}\n \t\tstrvec_push(&conn->args, cmd.buf);\ndiff --git a/t/t57xx-ssh-options-config.sh b/t/t57xx-ssh-options-config.sh\nnew file mode 100755\nindex 0000000000..6db5c3fa0b\n--- /dev/null\n+++ b/t/t57xx-ssh-options-config.sh\n@@ -0,0 +1,198 @@\n+#!/bin/sh\n+\n+test_description='test git ssh options patch'\n+\n+. ./test-lib.sh\n+\n+write_script fake-ssh <<-\\EOF &&\n+echo \"ssh: $*\" >\"$TRASH_DIRECTORY/ssh-output\"\n+exit 0\n+EOF\n+\n+test_expect_success 'setup ssh wrapper' '\n+\tGIT_SSH=\"$PWD/fake-ssh\" &&\n+\texport GIT_SSH &&\n+\tGIT_SSH_VARIANT=ssh &&\n+\texport GIT_SSH_VARIANT &&\n+\texport TRASH_DIRECTORY &&\n+\t>\"$TRASH_DIRECTORY\"/ssh-output\n+'\n+\n+test_expect_success 'add remote' '\n+\tgit remote add origin git@myhost:owner/repo.git\n+'\n+\n+test_expect_success 'create branch' '\n+\tgit commit -m \"Empty commit\" --allow-empty && \\\n+\tgit branch foo\n+'\n+\n+expect_ssh () {\n+\ttest_when_finished '(cd \"$TRASH_DIRECTORY\" && rm -f ssh-expect && >ssh-output)' &&\n+\techo \"ssh: $@\" >\"$TRASH_DIRECTORY/ssh-expect\" &&\n+\t(cd \"$TRASH_DIRECTORY\" && test_cmp ssh-expect ssh-output)\n+}\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch-pack' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git fetch-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \\\n+\t\t   -o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t   \"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch-pack and ssh:// uri' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git fetch-pack ssh://git@myhost/owner/repo.git &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \\\n+\t\t   -o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t   \"git-upload-pack '\\''/owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ send-pack' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git send-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -i /.ssh/id_test \\\n+\t\t-o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ ls-remote' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git ls-remote &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \\\n+\t\t-o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git fetch &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \\\n+\t\t-o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshIdentityFile is passed to ssh w/ push' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_must_fail git push origin foo &&\n+\texpect_ssh -i /.ssh/id_test \\\n+\t\t-o IdentitiesOnly=yes -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ fetch-pack' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git fetch-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ fetch-pack and ssh:// uri' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git fetch-pack ssh://git@myhost/owner/repo.git &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''/owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ send-pack' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git send-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -v -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ ls-remote' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git ls-remote &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ fetch' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git fetch &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'core sshOpts is passed to ssh w/ push' '\n+\ttest_config core.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git push origin foo &&\n+\texpect_ssh -v -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'owner overrides core on fetch-pack' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_config core.sshIdentityFile.owner /.ssh/id_test_owner &&\n+\ttest_config core.sshOpts \"-v\" &&\n+\ttest_config core.sshOpts.owner \"-v -F /dev/null\" &&\n+\ttest_must_fail git fetch-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_owner \\\n+\t\t-o IdentitiesOnly=yes -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'owner overrides core on send-pack' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_config core.sshIdentityFile.owner /.ssh/id_test_owner &&\n+\ttest_config core.sshOpts \"-v\" &&\n+\ttest_config core.sshOpts.owner \"-v -F /dev/null\" &&\n+\ttest_must_fail git send-pack git@myhost:owner/repo.git &&\n+\texpect_ssh -i /.ssh/id_test_owner -o IdentitiesOnly=yes \\\n+\t\t-v -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'remote overrides core on ls-remote' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_config remote.origin.sshIdentityFile /.ssh/id_test_remote &&\n+\ttest_config core.sshOpts \"-v\" &&\n+\ttest_config remote.origin.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git ls-remote &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_remote \\\n+\t\t-o IdentitiesOnly=yes -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'remote overrides core on fetch' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_config remote.origin.sshIdentityFile /.ssh/id_test_remote &&\n+\ttest_config core.sshOpts \"-v\" &&\n+\ttest_config remote.origin.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git fetch &&\n+\texpect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_remote \\\n+\t\t-o IdentitiesOnly=yes -v -F /dev/null git@myhost \\\n+\t\t\"git-upload-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'remote overrides core on push' '\n+\ttest_config core.sshIdentityFile /.ssh/id_test &&\n+\ttest_config remote.origin.sshIdentityFile /.ssh/id_test_remote &&\n+\ttest_config core.sshOpts \"-v\" &&\n+\ttest_config remote.origin.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git push origin foo &&\n+\texpect_ssh -i /.ssh/id_test_remote -o IdentitiesOnly=yes \\\n+\t\t-v -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success \"remote no SSH identity file or sshOpts are not injected\" '\n+\ttest_must_fail git push origin foo &&\n+\texpect_ssh git@myhost \"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+test_expect_success 'remote overrides owner on push' '\n+\ttest_config core.sshIdentityFile.owner /.ssh/id_test &&\n+\ttest_config remote.origin.sshIdentityFile /.ssh/id_test_remote &&\n+\ttest_config core.sshOpts.owner \"-v\" &&\n+\ttest_config remote.origin.sshOpts \"-v -F /dev/null\" &&\n+\ttest_must_fail git push origin foo &&\n+\texpect_ssh -i /.ssh/id_test_remote -o IdentitiesOnly=yes \\\n+\t\t-v -F /dev/null git@myhost \\\n+\t\t\"git-receive-pack '\\''owner/repo.git'\\''\"\n+'\n+\n+\n+test_done\n-- \n2.53.0.722.g8e572876c5\n\n"},{"id":"540168","messageId":"7d3731c5-d766-47f5-af60-813b379cbeef@kdbg.org","threadId":"65366","inReplyTo":"20260326233739.2911354-1-wesleys@opperschaap.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-27T07:51:32Z","receivedAt":"2026-03-27T07:51:42Z","isPatch":true,"body":"Am 27.03.26 um 00:37 schrieb Wesley Schwengle:\n> * `remote.*.sshIdentityFile' and `remote.*.sshOpts'\n> \n> Configuration set on owner/path style. This is to support `includeIf`\n> configuration management. For example, a git-forge that host both\n> employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and\n> `git@gitlab.com/corp/git.git' would have something configured as:\n> \n> * `core.sshIdentityFile.*', eg\n> \n>     [core \"sshIdentityFile\"]\n>       waterkip = ~/.ssh/id_ed25519_me\n>       corp     = ~/.ssh/id_ed25519_corporate\n\nThis can be solved without a changing Git today. You configure the two\nremotes with different fake host names:\n\n[remote \"waterkip\"]\n  url = git@waterkip.gitlab/waterkip/git.git\n[remote \"corp\"]\n  url = git@corp.gitlab/corp/git.git\n\nAnd set up the real host name and identity file in ~/.ssh/config:\n\nHost waterkip.gitlab\n  IdentityFile ~/.ssh/id_ed25519_me\n  HostName gitlab.com\n\nHost corp.gitlab\n  IdentityFile ~/.ssh/id_ed25519_corporate\n  HostName gitlab.com\n\n\nFor this reason, I see little incentive to add complexity to Git that\nachieves the same.\n\n-- Hannes\n\n"},{"id":"540182","messageId":"abe2616d-4dbc-4d84-9fa9-a6d24cd65927@opperschaap.net","threadId":"65366","inReplyTo":"7d3731c5-d766-47f5-af60-813b379cbeef@kdbg.org","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-27T15:04:01Z","receivedAt":"2026-03-27T15:04:14Z","isPatch":true,"body":"On 3/27/26 03:51, Johannes Sixt wrote:\n> Am 27.03.26 um 00:37 schrieb Wesley Schwengle:\n>> * `remote.*.sshIdentityFile' and `remote.*.sshOpts'\n>>\n>> Configuration set on owner/path style. This is to support `includeIf`\n>> configuration management. For example, a git-forge that host both\n>> employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and\n>> `git@gitlab.com/corp/git.git' would have something configured as:\n>>\n>> * `core.sshIdentityFile.*', eg\n>>\n>>      [core \"sshIdentityFile\"]\n>>        waterkip = ~/.ssh/id_ed25519_me\n>>        corp     = ~/.ssh/id_ed25519_corporate\n\n> For this reason, I see little incentive to add complexity to Git that\n> achieves the same.\n\nIt's a hacky solution where you change the ssh configuration \npermanently. It breaks copy/paste(s) etc for every forge.\n\nIn addition, this is where my need came from: It breaks myrepo's \nconfiguration(s) for people if they have to override the hostname in \neach myrepos config. You cannot simply override the hostname in these \nsitutations because of a local ssh config change.\n\nCheers,\nWesley\n\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540190","messageId":"xmqqbjg9mex2.fsf@gitster.g","threadId":"65366","inReplyTo":"7d3731c5-d766-47f5-af60-813b379cbeef@kdbg.org","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-27T16:10:33Z","receivedAt":"2026-03-27T16:10:37Z","isPatch":true,"body":"Johannes Sixt <j6t@kdbg.org> writes:\n\n> Am 27.03.26 um 00:37 schrieb Wesley Schwengle:\n>> * `remote.*.sshIdentityFile' and `remote.*.sshOpts'\n>> \n>> Configuration set on owner/path style. This is to support `includeIf`\n>> configuration management. For example, a git-forge that host both\n>> employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and\n>> `git@gitlab.com/corp/git.git' would have something configured as:\n>> \n>> * `core.sshIdentityFile.*', eg\n>> \n>>     [core \"sshIdentityFile\"]\n>>       waterkip = ~/.ssh/id_ed25519_me\n>>       corp     = ~/.ssh/id_ed25519_corporate\n>\n> This can be solved without a changing Git today. You configure the two\n> remotes with different fake host names:\n>\n> [remote \"waterkip\"]\n>   url = git@waterkip.gitlab/waterkip/git.git\n> [remote \"corp\"]\n>   url = git@corp.gitlab/corp/git.git\n\n> And set up the real host name and identity file in ~/.ssh/config:\n>\n> Host waterkip.gitlab\n>   IdentityFile ~/.ssh/id_ed25519_me\n>   HostName gitlab.com\n>\n> Host corp.gitlab\n>   IdentityFile ~/.ssh/id_ed25519_corporate\n>   HostName gitlab.com\n>\n>\n> For this reason, I see little incentive to add complexity to Git that\n> achieves the same.\n\nVery well said.\n\nI somehow thought that this practice is so widespread that it was\none of the few first things any new people learn to do, but perhaps\nwe do not have a good documentation coverage?\n\nIn any case, I do not think these network/transport specific\nconfiguration would hardly belong to \"core\".\n"},{"id":"540203","messageId":"09c5fe7d-8379-4f68-bf1c-9869e2924cb8@opperschaap.net","threadId":"65366","inReplyTo":"xmqqbjg9mex2.fsf@gitster.g","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-27T16:49:35Z","receivedAt":"2026-03-27T16:49:44Z","isPatch":true,"body":"On 3/27/26 12:10, Junio C Hamano wrote:\n\n> I somehow thought that this practice is so widespread that it was\n> one of the few first things any new people learn to do, but perhaps\n> we do not have a good documentation coverage?\n\nAs said before it is weird thing to configure a global ssh configuration\njust for git transport. It doesn't make much sense.\n\nThe problem with ssh_config usage is that you need to change your ssh \nconfig, which is machine global, not just git. And not portable across \nteams with configurations committed to git. Myrepos is a good example of \nthis. My former employer had this and I know the Perl metacpan project \nalso uses mysrepos. Changing every URL dynamically in committed configs \nisn't really a nice ask.\n\nThe alternative is using core.sshCommand to inject the correct keys, but \nyou must apply logic there when you have multiple accounts or forges. \nWhich is what I initially did with a zsh-scripts.\nWhich is why I ported that logic to git itself, I thought it would be \nbeneficial to have an easy way to maintain sshIdentityFile settings.\n\nIn addition, for core.sshCommand to work you must use the full openssh \ncommand rather than just adding some options to it. Which is an added \nbenefit of the proposed changes.\n\nThis change makes key selection possible without too much trouble on the \nusers side with hacks to ssh_config. You can just tell git to use an \nidentity based on the remote. Solve a git identify problem in the git \nconfig, fix the problem in the correct domain. We also store email \ncredentials in gitconfigs, why would an ssh identify file be treated \ndifferent?\n\n> In any case, I do not think these network/transport specific\n> configuration would hardly belong to \"core\".\n\nI'm happy to move it elsewhere, as said, I chose core because \ncore.sshCommand. As for the name: \"ssh\" or \"transport\", I'm not certain \nwhat is the best option is.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540237","messageId":"20260327213308.GA598533@coredump.intra.peff.net","threadId":"65366","inReplyTo":"20260326233739.2911354-2-wesleys@opperschaap.net","subject":"Re: [PATCH 1/3] connect: Rename name to command in connect_git()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-27T21:33:08Z","receivedAt":"2026-03-27T21:33:15Z","isPatch":true,"body":"On Thu, Mar 26, 2026 at 07:37:36PM -0400, Wesley Schwengle wrote:\n\n> connect_git has `char *name' in its signature and it caught me a little\n> offguard. I initially thought it was the remote name. But when you look\n> closer at the various call sites it is actually a command that is send\n> over the wire, eg . `git-receive-pack'. Change the naming makes it\n> easier to read the code and understand its intention.\n\nI agree that \"name\" is not all that descriptive, but I think there's a\nhidden gotcha in the explanation above. This string is _not_ the command\nthat we send over the wire. That's \"prog\" in the same function. And the\nreason that \"name\" exists is that it is a stable name for the operation\nwe are performing, like \"git-receive-pack\", even if configuration or\ncommand-line parameters (like \"--receive-pack=foo\") tell us to use a\ndifferent command name.\n\nSo probably \"op\" or \"type\" is a more accurate description. This\nconceptually ought to be an enum, too, since it is selecting from a\nlimited set of operations we know about.\n\nI took a quick stab at converting it to an enum (see below) and it's\nmostly an improvement, but:\n\n  1. The ripple effect went much farther than I expected, since the\n     transport code is passing these values, too. If we are going to\n     update one function in the chain, we should probably do all of them\n     (even if it is just a change of the variable name).\n\n  2. We end up having to convert to a string at some points anyway for\n     producing error messages, and for passing across the remote-helper\n     barrier. But I think we are still better off, because it's more\n     clear where we are using the string-ified version and what values\n     it could take.\n\n-Peff\n\n---\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex 13ea7308c8..3c1288a123 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -31,7 +31,7 @@ static int run_remote_archiver(int argc, const char **argv,\n \n \t_remote = remote_get(remote);\n \ttransport = transport_get(_remote, _remote->url.v[0]);\n-\ttransport_connect(transport, \"git-upload-archive\", exec, fd);\n+\ttransport_connect(transport, GIT_CONNECT_UPLOAD_ARCHIVE, exec, fd);\n \n \t/*\n \t * Inject a fake --format field at the beginning of the\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex d9e42bad58..316badd969 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -223,7 +223,7 @@ int cmd_fetch_pack(int argc,\n \t\tint flags = args.verbose ? CONNECT_VERBOSE : 0;\n \t\tif (args.diag_url)\n \t\t\tflags |= CONNECT_DIAG_URL;\n-\t\tconn = git_connect(fd, dest, \"git-upload-pack\",\n+\t\tconn = git_connect(fd, dest, GIT_CONNECT_UPLOAD_PACK,\n \t\t\t\t   args.uploadpack, flags);\n \t\tif (!conn)\n \t\t\treturn args.diag_url ? 0 : 1;\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8b81c8a848..1412b49bc8 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -273,8 +273,9 @@ int cmd_send_pack(int argc,\n \t\tfd[0] = 0;\n \t\tfd[1] = 1;\n \t} else {\n-\t\tconn = git_connect(fd, dest, \"git-receive-pack\", receivepack,\n-\t\t\targs.verbose ? CONNECT_VERBOSE : 0);\n+\t\tconn = git_connect(fd, dest, GIT_CONNECT_RECEIVE_PACK,\n+\t\t\t\t   receivepack,\n+\t\t\t\t   args.verbose ? CONNECT_VERBOSE : 0);\n \t}\n \n \tpacket_reader_init(&reader, fd[0], NULL, 0,\ndiff --git a/connect.c b/connect.c\nindex a02583a102..dad1cff1a8 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1428,6 +1428,7 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n  */\n struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\t  const char *name,\n+\t\t\t\t  enum git_connect_type type,\n \t\t\t\t  const char *prog, int flags)\n {\n \tchar *hostandport, *path;\n@@ -1441,7 +1442,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t * fetch, ls-remote, etc), then fallback to v0 since we don't know how\n \t * to do anything else (like push or remote archive) via v2.\n \t */\n-\tif (version == protocol_v2 && strcmp(\"git-upload-pack\", name))\n+\tif (version == protocol_v2 && type != GIT_CONNECT_UPLOAD_PACK)\n \t\tversion = protocol_v0;\n \n \t/* Without this we cannot rely on waitpid() to tell\ndiff --git a/connect.h b/connect.h\nindex 1645126c17..641498c759 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -7,7 +7,12 @@\n #define CONNECT_DIAG_URL      (1u << 1)\n #define CONNECT_IPV4          (1u << 2)\n #define CONNECT_IPV6          (1u << 3)\n-struct child_process *git_connect(int fd[2], const char *url, const char *name, const char *prog, int flags);\n+enum git_connect_type {\n+    GIT_CONNECT_UPLOAD_PACK,\n+    GIT_CONNECT_RECEIVE_PACK,\n+    GIT_CONNECT_UPLOAD_ARCHIVE,\n+};\n+struct child_process *git_connect(int fd[2], const char *url, enum git_connect_type, const char *prog, int flags);\n int finish_connect(struct child_process *conn);\n int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\ndiff --git a/transport-helper.c b/transport-helper.c\nindex 4d95d84f9e..c7fab6f560 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -620,8 +620,22 @@ static int run_connect(struct transport *transport, struct strbuf *cmdbuf)\n \treturn ret;\n }\n \n+static const char *connect_type_to_command(enum git_connect_type type)\n+{\n+\tswitch (type) {\n+\tcase GIT_CONNECT_UPLOAD_PACK:\n+\t\treturn \"git-upload-pack\";\n+\tcase GIT_CONNECT_RECEIVE_PACK:\n+\t\treturn \"git-receive-pack\";\n+\tcase GIT_CONNECT_UPLOAD_ARCHIVE:\n+\t\treturn \"git-upload-archive\";\n+\t}\n+\tBUG(\"unknown git_connect_type: %d\", type);\n+}\n+\n static int process_connect_service(struct transport *transport,\n-\t\t\t\t   const char *name, const char *exec)\n+\t\t\t\t   enum git_connect_type type,\n+\t\t\t\t   const char *exec)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf cmdbuf = STRBUF_INIT;\n@@ -631,7 +645,7 @@ static int process_connect_service(struct transport *transport,\n \t * Handle --upload-pack and friends. This is fire and forget...\n \t * just warn if it fails.\n \t */\n-\tif (strcmp(name, exec)) {\n+\tif (strcmp(connect_type_to_command(type), exec)) {\n \t\tint r = set_helper_option(transport, \"servpath\", exec);\n \t\tif (r > 0)\n \t\t\twarning(_(\"setting remote service path not supported by protocol\"));\n@@ -640,13 +654,13 @@ static int process_connect_service(struct transport *transport,\n \t}\n \n \tif (data->connect) {\n-\t\tstrbuf_addf(&cmdbuf, \"connect %s\\n\", name);\n+\t\tstrbuf_addf(&cmdbuf, \"connect %s\\n\", connect_type_to_command(type));\n \t\tret = run_connect(transport, &cmdbuf);\n \t} else if (data->stateless_connect &&\n \t\t   (get_protocol_version_config() == protocol_v2) &&\n-\t\t   (!strcmp(\"git-upload-pack\", name) ||\n-\t\t    !strcmp(\"git-upload-archive\", name))) {\n-\t\tstrbuf_addf(&cmdbuf, \"stateless-connect %s\\n\", name);\n+\t\t   (type == GIT_CONNECT_UPLOAD_PACK ||\n+\t\t    type == GIT_CONNECT_UPLOAD_ARCHIVE)) {\n+\t\tstrbuf_addf(&cmdbuf, \"stateless-connect %s\\n\", connect_type_to_command(type));\n \t\tret = run_connect(transport, &cmdbuf);\n \t\tif (ret)\n \t\t\ttransport->stateless_rpc = 1;\n@@ -660,32 +674,33 @@ static int process_connect(struct transport *transport,\n \t\t\t\t     int for_push)\n {\n \tstruct helper_data *data = transport->data;\n-\tconst char *name;\n+\tenum git_connect_type type;\n \tconst char *exec;\n \tint ret;\n \n-\tname = for_push ? \"git-receive-pack\" : \"git-upload-pack\";\n+\ttype = for_push ? GIT_CONNECT_RECEIVE_PACK : GIT_CONNECT_UPLOAD_PACK;\n \tif (for_push)\n \t\texec = data->transport_options.receivepack;\n \telse\n \t\texec = data->transport_options.uploadpack;\n \n-\tret = process_connect_service(transport, name, exec);\n+\tret = process_connect_service(transport, type, exec);\n \tif (ret)\n \t\tdo_take_over(transport);\n \treturn ret;\n }\n \n-static int connect_helper(struct transport *transport, const char *name,\n-\t\t   const char *exec, int fd[2])\n+static int connect_helper(struct transport *transport, enum git_connect_type type,\n+\t\t\t  const char *exec, int fd[2])\n {\n \tstruct helper_data *data = transport->data;\n \n \t/* Get_helper so connect is inited. */\n \tget_helper(transport);\n \n-\tif (!process_connect_service(transport, name, exec))\n-\t\tdie(_(\"can't connect to subservice %s\"), name);\n+\tif (!process_connect_service(transport, type, exec))\n+\t\tdie(_(\"can't connect to subservice %s\"),\n+\t\t    connect_type_to_command(type));\n \n \tfd[0] = data->helper->out;\n \tfd[1] = data->helper->in;\ndiff --git a/transport-internal.h b/transport-internal.h\nindex 90ea749e5c..1a86c63ce0 100644\n--- a/transport-internal.h\n+++ b/transport-internal.h\n@@ -58,7 +58,7 @@ struct transport_vtable {\n \t * process involved generating new commits.\n \t **/\n \tint (*push_refs)(struct transport *transport, struct ref *refs, int flags);\n-\tint (*connect)(struct transport *connection, const char *name,\n+\tint (*connect)(struct transport *connection, enum git_connect_type type,\n \t\t       const char *executable, int fd[2]);\n \n \t/** get_refs_list(), fetch(), and push_refs() can keep\ndiff --git a/transport.c b/transport.c\nindex cb1befba8c..2fd94d701f 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -308,8 +308,8 @@ static int connect_setup(struct transport *transport, int for_push)\n \n \tdata->conn = git_connect(data->fd, transport->url,\n \t\t\t\t for_push ?\n-\t\t\t\t\t\"git-receive-pack\" :\n-\t\t\t\t\t\"git-upload-pack\",\n+\t\t\t\t\tGIT_CONNECT_RECEIVE_PACK :\n+\t\t\t\t\tGIT_CONNECT_UPLOAD_PACK,\n \t\t\t\t for_push ?\n \t\t\t\t\tdata->options.receivepack :\n \t\t\t\t\tdata->options.uploadpack,\n@@ -956,12 +956,12 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re\n \treturn ret;\n }\n \n-static int connect_git(struct transport *transport, const char *name,\n+static int connect_git(struct transport *transport, enum git_connect_type type,\n \t\t       const char *executable, int fd[2])\n {\n \tstruct git_transport_data *data = transport->data;\n \tdata->conn = git_connect(data->fd, transport->url,\n-\t\t\t\t name, executable, 0);\n+\t\t\t\t type, executable, 0);\n \tfd[0] = data->fd[0];\n \tfd[1] = data->fd[1];\n \treturn 0;\n@@ -1650,11 +1650,11 @@ void transport_unlock_pack(struct transport *transport, unsigned int flags)\n \t\tstring_list_clear(&transport->pack_lockfiles, 0);\n }\n \n-int transport_connect(struct transport *transport, const char *name,\n+int transport_connect(struct transport *transport, enum git_connect_type type,\n \t\t      const char *exec, int fd[2])\n {\n \tif (transport->vtable->connect)\n-\t\treturn transport->vtable->connect(transport, name, exec, fd);\n+\t\treturn transport->vtable->connect(transport, type, exec, fd);\n \telse\n \t\tdie(_(\"operation not supported by protocol\"));\n }\ndiff --git a/transport.h b/transport.h\nindex 892f19454a..1e6fd263f6 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -5,6 +5,7 @@\n #include \"remote.h\"\n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"connect.h\"\n \n struct git_transport_options {\n \tunsigned thin : 1;\n@@ -324,7 +325,7 @@ char *transport_anonymize_url(const char *url);\n void transport_take_over(struct transport *transport,\n \t\t\t struct child_process *child);\n \n-int transport_connect(struct transport *transport, const char *name,\n+int transport_connect(struct transport *transport, enum git_connect_type type,\n \t\t      const char *exec, int fd[2]);\n \n /* Transport methods defined outside transport.c */\n"},{"id":"540238","messageId":"20260327213954.GB598533@coredump.intra.peff.net","threadId":"65366","inReplyTo":"20260326233739.2911354-3-wesleys@opperschaap.net","subject":"Re: [PATCH 2/3] connect: Add transport->remote->name to git_connect()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-27T21:39:54Z","receivedAt":"2026-03-27T21:39:55Z","isPatch":true,"body":"On Thu, Mar 26, 2026 at 07:37:37PM -0400, Wesley Schwengle wrote:\n\n> To support `remote.$name.sshIdentityFile', and `remote.$name.sshOpts' for\n> connecting to various remotes I need to pass around the remote down to\n> git_connect. This commit introduces the `remote_name' and sprinkles all\n> call sites to pass `NULL'. This is a non-breaking forward change\n\nMy gut feeling is here is that this is going to be the wrong level for\nreading config, because it's too low and too coarse. If we ever want to\nhave a command-line option for overriding config, like \"git fetch\n--ssh-identity-file=foo\", then how can the higher level git-fetch code\npass down that single item?\n\nI.e., I think the ideal form of this would be that we pass around an\nssh_options_context struct, high-level commands fill in that struct\nbased on command-line options or config (including remote-specific\nones), and then we act on it at the lowest level when spawning ssh.\n\n\nAll that said, my first thought here is that most of what this series\ndoes is already possible with ssh config. It looks like that has already\nbeen suggested elsewhere in the thread, so I'll go read that before\ncommenting further.\n\n-Peff\n"},{"id":"540239","messageId":"20260327214559.GA599365@coredump.intra.peff.net","threadId":"65366","inReplyTo":"20260326233739.2911354-4-wesleys@opperschaap.net","subject":"Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-27T21:45:59Z","receivedAt":"2026-03-27T21:46:01Z","isPatch":true,"body":"On Thu, Mar 26, 2026 at 07:37:38PM -0400, Wesley Schwengle wrote:\n\n> The following configuration is supported, in order of precedence:\n> \n>   1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts'\n> \n>   2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>'\n> \n>      Where <owner> is derived from the repository path. Nested groups\n>      aren't supported: git@host:owner/repo.git becomes \"owner\",\n>      git@host:owner/group/repo.git also becomes \"owner\".\n\nWe already have some conditional config mechanisms, and I don't think\nit's a good idea to add one that only works for certain keys. If I\nunderstand correctly, this <owner> feature can already be accomplished\nwith:\n\n  [includeIf \"hasconfig:remote.*.url:**/owner/**\"]\n  path = all-your-options-for-that-owner\n\nIt's a little more verbose (and you have to use a separate file), but it\nalso allows other conditions, like \"gitdir:\" for selecting based on how\nyou lay out your repos locally.\n\n-Peff\n"},{"id":"540240","messageId":"acb7zRGLkQUSIZkU@fruit.crustytoothpaste.net","threadId":"65366","inReplyTo":"xmqqbjg9mex2.fsf@gitster.g","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-27T21:51:09Z","receivedAt":"2026-03-27T21:51:18Z","isPatch":true,"body":"On 2026-03-27 at 16:10:33, Junio C Hamano wrote:\n> I somehow thought that this practice is so widespread that it was\n> one of the few first things any new people learn to do, but perhaps\n> we do not have a good documentation coverage?\n\nI actually added this to the Git FAQ:\nhttps://git-scm.com/docs/gitfaq#multiple-accounts-ssh.  It was added\nbecause I saw the question a lot online but we never documented how to\ndo this.\n\nCertainly we might want to improve the documentation (patches welcome),\nbut I would not honestly say we have bad documentation coverage here.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"540241","messageId":"acb_SQ8gdy-fQaFj@fruit.crustytoothpaste.net","threadId":"65366","inReplyTo":"09c5fe7d-8379-4f68-bf1c-9869e2924cb8@opperschaap.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-27T22:06:01Z","receivedAt":"2026-03-27T22:06:03Z","isPatch":true,"body":"On 2026-03-27 at 16:49:35, Wesley wrote:\n> On 3/27/26 12:10, Junio C Hamano wrote:\n> \n> > I somehow thought that this practice is so widespread that it was\n> > one of the few first things any new people learn to do, but perhaps\n> > we do not have a good documentation coverage?\n> \n> As said before it is weird thing to configure a global ssh configuration\n> just for git transport. It doesn't make much sense.\n> \n> The problem with ssh_config usage is that you need to change your ssh\n> config, which is machine global, not just git. And not portable across teams\n> with configurations committed to git. Myrepos is a good example of this. My\n> former employer had this and I know the Perl metacpan project also uses\n> mysrepos. Changing every URL dynamically in committed configs isn't really a\n> nice ask.\n\nYou can also use the conditional inclusion functionality to rewrite URLs\nfor repositories in a certain directory with `url.<URL>.insteadOf`.  Or\nyou can use conditional inclusion to use `core.sshCommand` with the `-i`\noption set appropriately.\n\n> The alternative is using core.sshCommand to inject the correct keys, but you\n> must apply logic there when you have multiple accounts or forges. Which is\n> what I initially did with a zsh-scripts.\n> Which is why I ported that logic to git itself, I thought it would be\n> beneficial to have an easy way to maintain sshIdentityFile settings.\n> \n> In addition, for core.sshCommand to work you must use the full openssh\n> command rather than just adding some options to it. Which is an added\n> benefit of the proposed changes.\n\nRight, but the additional burden is typing \"ssh -i\" for that option.\nThat's not very substantial.  And the existing option is much more\nflexible as well, since it allows you to use other options, such as `-o\nControlMaster`, which is useful when you're using a security key and\ndon't want to re-authenticate all the time.  It also allows you to use\narbitrary shell scripting, too, which means that you can customize\nthe configuration depending on what keys are available or what machine\nyou're on (or really anything else).\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"540243","messageId":"xmqqo6k8j4ey.fsf@gitster.g","threadId":"65366","inReplyTo":"acb7zRGLkQUSIZkU@fruit.crustytoothpaste.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-27T22:25:41Z","receivedAt":"2026-03-27T22:25:44Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> On 2026-03-27 at 16:10:33, Junio C Hamano wrote:\n>> I somehow thought that this practice is so widespread that it was\n>> one of the few first things any new people learn to do, but perhaps\n>> we do not have a good documentation coverage?\n>\n> I actually added this to the Git FAQ:\n> https://git-scm.com/docs/gitfaq#multiple-accounts-ssh.  It was added\n> because I saw the question a lot online but we never documented how to\n> do this.\n>\n> Certainly we might want to improve the documentation (patches welcome),\n> but I would not honestly say we have bad documentation coverage here.\n\nOK, so that is not lack of documentation but insufficient searching\n;-).\n\n"},{"id":"540251","messageId":"a4a03bae-b987-4b21-a7fd-fbdb9d832430@opperschaap.net","threadId":"65366","inReplyTo":"20260327214559.GA599365@coredump.intra.peff.net","subject":"Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T00:43:07Z","receivedAt":"2026-03-28T00:43:17Z","isPatch":true,"body":"On 3/27/26 17:45, Jeff King wrote:\n> On Thu, Mar 26, 2026 at 07:37:38PM -0400, Wesley Schwengle wrote:\n> \n>> The following configuration is supported, in order of precedence:\n>>\n>>    1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts'\n>>\n>>    2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>'\n>>\n>>       Where <owner> is derived from the repository path. Nested groups\n>>       aren't supported: git@host:owner/repo.git becomes \"owner\",\n>>       git@host:owner/group/repo.git also becomes \"owner\".\n> \n> We already have some conditional config mechanisms, and I don't think\n> it's a good idea to add one that only works for certain keys. If I\n> understand correctly, this <owner> feature can already be accomplished\n> with:\n> \n>    [includeIf \"hasconfig:remote.*.url:**/owner/**\"]\n>    path = all-your-options-for-that-owner\n> \n> It's a little more verbose (and you have to use a separate file), but it\n> also allows other conditions, like \"gitdir:\" for selecting based on how\n> you lay out your repos locally.\n\nThis doesn't work as you would think it does. The includeIf on hasconfig \nwith the remote URL is used if it finds the remote in the config, and \nnot on the actual network action. Thus if you have two remotes with two \nincludeIfs on the remote URL it takes the config of the last defined \ninclude. Thus breaks the expectation that it is configured.\n\nCheers,\nWesley\n\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540252","messageId":"3e9d8d71-9595-4151-8133-300b89b3b7f8@opperschaap.net","threadId":"65366","inReplyTo":"20260327213308.GA598533@coredump.intra.peff.net","subject":"Re: [PATCH 1/3] connect: Rename name to command in connect_git()","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T00:58:22Z","receivedAt":"2026-03-28T00:58:30Z","isPatch":true,"body":"On 3/27/26 17:33, Jeff King wrote:\n> On Thu, Mar 26, 2026 at 07:37:36PM -0400, Wesley Schwengle wrote:\n> \n>> connect_git has `char *name' in its signature and it caught me a little\n>> offguard. I initially thought it was the remote name. But when you look\n>> closer at the various call sites it is actually a command that is send\n>> over the wire, eg . `git-receive-pack'. Change the naming makes it\n>> easier to read the code and understand its intention.\n> \n> I agree that \"name\" is not all that descriptive, but I think there's a\n> hidden gotcha in the explanation above. This string is _not_ the command\n> that we send over the wire. That's \"prog\" in the same function. And the\n> reason that \"name\" exists is that it is a stable name for the operation\n> we are performing, like \"git-receive-pack\", even if configuration or\n> command-line parameters (like \"--receive-pack=foo\") tell us to use a\n> different command name.\n> \n> So probably \"op\" or \"type\" is a more accurate description. This\n> conceptually ought to be an enum, too, since it is selecting from a\n> limited set of operations we know about.\n\nThat's a fair take on it, \"name\" is really a not the best name for this \nvariable. I think \"op\" covers what you describe here best, it reflects \nalso why I named it command. When you check what is sent via ssh, it \nlooks like the command:\n\n    ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack \n'waterkip/git.git'\n\nThat's why in my change it was named command, op, or operation covers it \ntoo.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540253","messageId":"8c6cb953-e4b5-40c2-9fc3-734ffee7f313@opperschaap.net","threadId":"65366","inReplyTo":"acb_SQ8gdy-fQaFj@fruit.crustytoothpaste.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T01:02:53Z","receivedAt":"2026-03-28T01:02:59Z","isPatch":true,"body":"On 3/27/26 18:06, brian m. carlson wrote:\n> On 2026-03-27 at 16:49:35, Wesley wrote:\n>> On 3/27/26 12:10, Junio C Hamano wrote:\n>>\n>>> I somehow thought that this practice is so widespread that it was\n>>> one of the few first things any new people learn to do, but perhaps\n>>> we do not have a good documentation coverage?\n>>\n>> As said before it is weird thing to configure a global ssh configuration\n>> just for git transport. It doesn't make much sense.\n>>\n>> The problem with ssh_config usage is that you need to change your ssh\n>> config, which is machine global, not just git. And not portable across teams\n>> with configurations committed to git. Myrepos is a good example of this. My\n>> former employer had this and I know the Perl metacpan project also uses\n>> mysrepos. Changing every URL dynamically in committed configs isn't really a\n>> nice ask.\n> \n> You can also use the conditional inclusion functionality to rewrite URLs\n> for repositories in a certain directory with `url.<URL>.insteadOf`.  Or\n> you can use conditional inclusion to use `core.sshCommand` with the `-i`\n> option set appropriately.\n\nThat is what I did and why I thought a simple addition in git would make \nit declarative in git via its configuration. This would limit the \nscripting side for just adding \"this identityFile should be used in this \nrepo\" or \"this remote\". Including allowing setting sshOpts for specific \nremotes and/or repos.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540254","messageId":"20260328014426.GA621762@coredump.intra.peff.net","threadId":"65366","inReplyTo":"3e9d8d71-9595-4151-8133-300b89b3b7f8@opperschaap.net","subject":"Re: [PATCH 1/3] connect: Rename name to command in connect_git()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-28T01:44:26Z","receivedAt":"2026-03-28T01:44:27Z","isPatch":true,"body":"On Fri, Mar 27, 2026 at 08:58:22PM -0400, Wesley wrote:\n\n> > So probably \"op\" or \"type\" is a more accurate description. This\n> > conceptually ought to be an enum, too, since it is selecting from a\n> > limited set of operations we know about.\n> \n> That's a fair take on it, \"name\" is really a not the best name for this\n> variable. I think \"op\" covers what you describe here best, it reflects also\n> why I named it command. When you check what is sent via ssh, it looks like\n> the command:\n> \n>    ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack\n> 'waterkip/git.git'\n\nRight, but it's necessarily what is sent via ssh. E.g.:\n\n  $ GIT_TRACE=1 git ls-remote example.com:repo.git\n  [...]\n  trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'git-upload-pack '\\''repo.git'\\'''\n\n  $ GIT_TRACE=1 git ls-remote --upload-pack=foobar example.com:repo.git\n  [...]\n  trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'foobar '\\''repo.git'\\'''\n\nThat's why I think \"command\" is actively misleading, because between\n\"prog\" and \"command\" it is not clear which one is going to be sent to\nthe remote.\n\n-Peff\n"},{"id":"540255","messageId":"01ca1166-c3ad-48a9-8edf-be82d380e110@opperschaap.net","threadId":"65366","inReplyTo":"20260328014426.GA621762@coredump.intra.peff.net","subject":"Re: [PATCH 1/3] connect: Rename name to command in connect_git()","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T02:01:16Z","receivedAt":"2026-03-28T02:01:24Z","isPatch":true,"body":"On 3/27/26 21:44, Jeff King wrote:\n> On Fri, Mar 27, 2026 at 08:58:22PM -0400, Wesley wrote:\n> \n>>> So probably \"op\" or \"type\" is a more accurate description. This\n>>> conceptually ought to be an enum, too, since it is selecting from a\n>>> limited set of operations we know about.\n>>\n>> That's a fair take on it, \"name\" is really a not the best name for this\n>> variable. I think \"op\" covers what you describe here best, it reflects also\n>> why I named it command. When you check what is sent via ssh, it looks like\n>> the command:\n>>\n>>     ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack\n>> 'waterkip/git.git'\n> \n> Right, but it's necessarily what is sent via ssh. E.g.:\n> \n>    $ GIT_TRACE=1 git ls-remote example.com:repo.git\n>    [...]\n>    trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'git-upload-pack '\\''repo.git'\\'''\n> \n>    $ GIT_TRACE=1 git ls-remote --upload-pack=foobar example.com:repo.git\n>    [...]\n>    trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'foobar '\\''repo.git'\\'''\n> \n> That's why I think \"command\" is actively misleading, because between\n> \"prog\" and \"command\" it is not clear which one is going to be sent to\n> the remote.\n\n\nHa! Interesting. I see the confusion :)\nI'm not really sure what to call it.\n\nI see the manpage calls it 'exec':\n\n   --upload-pack=<exec>\n      Specify the full path of git-upload-pack on the remote host. This\n      allows listing references from repositories accessed via SSH and\n      where the SSH daemon does not use the PATH configured by the user.\n\nand it's the full path of the git-upload-pack command if the remote \ndoesn't use the PATH. So it is command, just.. I'm not sure what to call \nit. It executable, binary, program, operation, script. I feel they all \ncover the same concept. remote-command? It could be any of them iyam.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540256","messageId":"20260328020327.GB621762@coredump.intra.peff.net","threadId":"65366","inReplyTo":"a4a03bae-b987-4b21-a7fd-fbdb9d832430@opperschaap.net","subject":"Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-28T02:03:27Z","receivedAt":"2026-03-28T02:03:28Z","isPatch":true,"body":"On Fri, Mar 27, 2026 at 08:43:07PM -0400, Wesley wrote:\n\n> > We already have some conditional config mechanisms, and I don't think\n> > it's a good idea to add one that only works for certain keys. If I\n> > understand correctly, this <owner> feature can already be accomplished\n> > with:\n> > \n> >    [includeIf \"hasconfig:remote.*.url:**/owner/**\"]\n> >    path = all-your-options-for-that-owner\n> > \n> > It's a little more verbose (and you have to use a separate file), but it\n> > also allows other conditions, like \"gitdir:\" for selecting based on how\n> > you lay out your repos locally.\n> \n> This doesn't work as you would think it does. The includeIf on hasconfig\n> with the remote URL is used if it finds the remote in the config, and not on\n> the actual network action. Thus if you have two remotes with two includeIfs\n> on the remote URL it takes the config of the last defined include. Thus\n> breaks the expectation that it is configured.\n\nYes, it's going to be per-local-repo. I had assumed you were in a\nsituation where you were defining these setups at the global level, and\neach local repo will want to use them or not. I.e., something like this:\n\n  [set up once]\n  $ git config -f ~/.gitconfig-foo core.sshCommand \"ssh -i whatever\"\n  $ git config --global includeIf.hasconfig:remote.*.url:example.com:foo/**.path .gitconfig-foo\n\n  [and now we'd use it in this repo]\n  $ git clone example.com:foo/repo.git\n\n  [but not this one]\n  $ git clone example.com:bar/repo.git\n\nIf you have remotes for both \"foo/repo.git\" and \"bar/repo.git\"\nconfigured in one local repo, then yes, it will always apply the config.\n\nIf you really want per-connection config, I'm still not quite convinced\nthat you aren't better off defining host sections in your ssh config.\nThat covers all options that ssh knows about (not just ones we teach Git\nabout), and you can still apply it automatically from ~/.gitconfig using\ninsteadOf. Something like:\n\n  git config --global foo.example.com:foo/.insteadOf example.com:foo/\n\nand then defining a foo.example.com block in your ~/.ssh/config.\n\n-Peff\n"},{"id":"540257","messageId":"c5ad8e2e-c361-4f75-b557-2e7dc119ac01@opperschaap.net","threadId":"65366","inReplyTo":"20260328020327.GB621762@coredump.intra.peff.net","subject":"Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T02:25:45Z","receivedAt":"2026-03-28T02:25:54Z","isPatch":true,"body":"On 3/27/26 22:03, Jeff King wrote:\n> On Fri, Mar 27, 2026 at 08:43:07PM -0400, Wesley wrote:\n> \nigured in one local repo, then yes, it will always apply the config.\n> \n> If you really want per-connection config, I'm still not quite convinced\n> that you aren't better off defining host sections in your ssh config.\n> That covers all options that ssh knows about (not just ones we teach Git\n> about), and you can still apply it automatically from ~/.gitconfig using\n> insteadOf. Something like:\n> \n>    git config --global foo.example.com:foo/.insteadOf example.com:foo/\n> \n> and then defining a foo.example.com block in your ~/.ssh/config.\n\nThis is where it breaks in my mind. I'm configuring ssh to configure git.\n\nBtw, I'm assuming you meant:\n\n     git config --global url.foo.example.com:foo/.insteadOf example.com:foo/\n\nI never took this approach with ssh identity files. I'll have a look at \nthis approach see how it works. The submitted patch approach has served \nme over a number of years, albeit not directly in C. I just store the \nconfig in git and I don't need to worry about ssh anymore.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540264","messageId":"becf040c-b425-4fd1-affa-b6368c812b42@kdbg.org","threadId":"65366","inReplyTo":"09c5fe7d-8379-4f68-bf1c-9869e2924cb8@opperschaap.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-28T07:46:46Z","receivedAt":"2026-03-28T07:47:01Z","isPatch":true,"body":"Am 27.03.26 um 17:49 schrieb Wesley:\n> On 3/27/26 12:10, Junio C Hamano wrote:\n>> I somehow thought that this practice is so widespread that it was\n>> one of the few first things any new people learn to do, but perhaps\n>> we do not have a good documentation coverage?\n> \n> As said before it is weird thing to configure a global ssh configuration\n> just for git transport. It doesn't make much sense.\n> \n> The problem with ssh_config usage is that you need to change your ssh\n> config, which is machine global, not just git.\n\nAre thinking about the SSH configuration in /etc/ssh? You do not have to\nchange that. There is also a .ssh/config in the user's home directory.\nThat configuration isn't machine global, it's obviously per user. And\nthe way to make the configuration work only for Git is precisely to use\nfake host names that are only used in remote URLs of Git repositories.\n\n> And not portable across\n> teams with configurations committed to git. Myrepos is a good example of\n> this. My former employer had this and I know the Perl metacpan project\n> also uses mysrepos. Changing every URL dynamically in committed configs\n> isn't really a nice ask.\n\nI cannot comment on this, because I do not know these tools.\n\nThere are ways to achieve a considerable amount of customization of SSH\nconnections with existing tools. If you need additional features, you\nshould sell your change with a more specific justification, including\nexamples that show reviewers who do not know the tools you are using\nwhat is needed, but missing.\n\n-- Hannes\n\n"},{"id":"540277","messageId":"3d8c9b3f-66d0-460d-bd61-a879a6bbfc56@opperschaap.net","threadId":"65366","inReplyTo":"becf040c-b425-4fd1-affa-b6368c812b42@kdbg.org","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Wesley","fromEmail":"wesleys@opperschaap.net","sentAt":"2026-03-28T14:59:47Z","receivedAt":"2026-03-28T15:00:00Z","isPatch":true,"body":"On 3/28/26 03:46, Johannes Sixt wrote:\n> Am 27.03.26 um 17:49 schrieb Wesley:\n>> On 3/27/26 12:10, Junio C Hamano wrote:\n>>> I somehow thought that this practice is so widespread that it was\n>>> one of the few first things any new people learn to do, but perhaps\n>>> we do not have a good documentation coverage?\n>>\n>> As said before it is weird thing to configure a global ssh configuration\n>> just for git transport. It doesn't make much sense.\n>>\n>> The problem with ssh_config usage is that you need to change your ssh\n>> config, which is machine global, not just git.\n> \n> Are thinking about the SSH configuration in /etc/ssh? You do not have to\n> change that. There is also a .ssh/config in the user's home directory.\n> That configuration isn't machine global, it's obviously per user. And\n> the way to make the configuration work only for Git is precisely to use\n> fake host names that are only used in remote URLs of Git repositories.\n\nI refered to that as the .ssh/config unit. But /etc/ssh/ssh_config is a \nmore global setting indeed.\n\n>> And not portable across\n>> teams with configurations committed to git. Myrepos is a good example of\n>> this. My former employer had this and I know the Perl metacpan project\n>> also uses mysrepos. Changing every URL dynamically in committed configs\n>> isn't really a nice ask.\n> \n> I cannot comment on this, because I do not know these tools.\n> \n> There are ways to achieve a considerable amount of customization of SSH\n> connections with existing tools. If you need additional features, you\n> should sell your change with a more specific justification, including\n> examples that show reviewers who do not know the tools you are using\n> what is needed, but missing.\n\nThe ways to do it all involve configuring ssh to configure git, instead \nof configuring git to configure git. The remote is already configured in \ngit, having your sshIndentityFile and possible other options close to \nthat configuration is beneficial to users. The escape-hatch of \ncore.sshCommand doesn't need to be utilized for a simple \"Use this \nindentityFile on this remote\".\n\nThe only way to configure git without touching ssh is to fiddle with the \ncore.sshCommand, which I did in my own zsh script. This script also \nutilized the git config, I used my own namespace for this, which in this \npatch became \"core\". The whole idea was: git owns git operations, thus \nthe config should live in git.\n\nThe need for me arose precisely because upstream encoded git repos on a \nforge where my personal projects also resided and forced me to create a \nsecond account. Having to change ssh config was to me the wrong knob to \nturn. I fixed it years ago, and while refactoring it I thought the \npattern would be helpful for every git user resulting in the above patch.\n\nI think this is helpful for freelancers who have multiple clients and \ndon't feel the need to add a specific host in their .ssh/config for each \nclient. They can includeIf it, setting repos with a particular \"owner\" \nto a specific identity file or they can set it on remote level basis if \nthe need is there. That is why the cascading configuration was added.\n\nThere is no need to configure both ssh and possible git with rewrite \nrules with this patch. Which to me is a cleaner solution. One knob in \ngit for git.\n\nCheers,\nWesley\n\n-- \nWesley\n\nWhy not both?\n"},{"id":"540326","messageId":"ACA4834A-2F3F-4817-B2BF-1EF8134FA02A@gmail.com","threadId":"65366","inReplyTo":"3d8c9b3f-66d0-460d-bd61-a879a6bbfc56@opperschaap.net","subject":"Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-03-29T14:33:58Z","receivedAt":"2026-03-29T14:34:11Z","isPatch":true,"body":"\n> Le 28 mars 2026 à 11:02, Wesley <wesleys@opperschaap.net> a écrit :\n> \n> ﻿On 3/28/26 03:46, Johannes Sixt wrote:\n> \n>>> And not portable across\n>>> teams with configurations committed to git. Myrepos is a good example of\n>>> this. My former employer had this and I know the Perl metacpan project\n>>> also uses mysrepos. Changing every URL dynamically in committed configs\n>>> isn't really a nice ask.\n>> I cannot comment on this, because I do not know these tools.\n>> There are ways to achieve a considerable amount of customization of SSH\n>> connections with existing tools. If you need additional features, you\n>> should sell your change with a more specific justification, including\n>> examples that show reviewers who do not know the tools you are using\n>> what is needed, but missing.\n> \n> The ways to do it all involve configuring ssh to configure git, instead of configuring git to configure git. The remote is already configured in git, having your sshIndentityFile and possible other options close to that configuration is beneficial to users. The escape-hatch of core.sshCommand doesn't need to be utilized for a simple \"Use this indentityFile on this remote\".\n\nHm. But if you’re connecting to a host (Git or otherwise) via SSH, isn’t the natural place to configure that the SSH client?\n\nI use custom host config to control identify files so that each host I connect to gets a unique identity; some are Git hosts, some are other things.\n\nI don’t yet deal with different connections to the same host, so the only Git configuration I make is to rewrite some HTTPS remote URLs to SSH connection strings. \n\n> The only way to configure git without touching ssh\n\nI think I, at least, claim that this is Git working with SSH, so it is natural that both sides may require tuning. \n\n> There is no need to configure both ssh and possible git with rewrite rules with this patch. Which to me is a cleaner solution. One knob in git for git.\n\nHence let each do what they do best :) each knob where it belongs.\n\nJust my 2¢. Cheers!"}]}