* [PATCH 0/3] Add support for per-remote and per-namespace SSH options
@ 2026-03-26 23:37 Wesley Schwengle
2026-03-26 23:37 ` [PATCH 1/3] connect: Rename name to command in connect_git() Wesley Schwengle
` (3 more replies)
0 siblings, 4 replies; 22+ messages in thread
From: Wesley Schwengle @ 2026-03-26 23:37 UTC (permalink / raw)
To: git
With this changeset applied git is now aware of `sshIdentityFiles' and
`sshOpts'. This allows users to have multiple accounts on the same forges.
A common problem within the developer community. This problem is often
solved by hacking in one's `.ssh/config' and changing hostname URIs to
ensure the correct key is being used.
For years I had zsh wrapper script that was used as the `core.sshCommand' and
is a reference implementation of this change.
In order of importance:
Configuration on the remotes itself. This is easy, straight forward and
should allow people to get it to work quickly:
* `remote.*.sshIdentityFile' and `remote.*.sshOpts'
Configuration set on owner/path style. This is to support `includeIf`
configuration management. For example, a git-forge that host both
employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and
`git@gitlab.com/corp/git.git' would have something configured as:
* `core.sshIdentityFile.*', eg
[core "sshIdentityFile"]
waterkip = ~/.ssh/id_ed25519_me
corp = ~/.ssh/id_ed25519_corporate
And finally, a global override for everything:
* `core.sshIdentityFile' and `core.sshOpts'
I stayed within the `core' namespace, mainly because `core.sshCommand'. I'm
happy to move it to `ssh' or something similar. It would perhaps make
`ssh.*.sshIdentifyFile' more structured, because now that's split between two
core subsections.
The following assumptions have been made to make it safe and sound for
users. When an `sshIdentityFile' is used and no `sshOpts' are configured git
will inject `-F /dev/null' to prevent cycling over all sshIdentityFiles
a user has in their `.ssh/config'. When a user configures `sshOpts', these
take precedence and a user itself is responsible for setting
`-F /dev/null'.
Separate push/pull URIs are not supported by the feature. The biggest problem
with this is that I don't know how to properly configure them with the
namespace constraints. `remote.*.xyz' is as deep as git can go and a push/pull
would require additional configuration. I filed it under edge-case.
There are two new structs introduced: `ssh_options' and `cnx_context'.
They are there to limit the amount of argument passing down the wire. And this
is especially true for `ssh_options' because it keeps `push_ssh_options' dumb.
Wesley Schwengle (3):
connect: Rename name to command in connect_git()
connect: Add transport->remote->name to git_connect()
connect: Add support for per-remote and per-namespace SSH options
Documentation/config/core.adoc | 22 ++++
Documentation/config/remote.adoc | 9 ++
builtin/fetch-pack.c | 2 +-
builtin/send-pack.c | 2 +-
connect.c | 144 ++++++++++++++++++++--
connect.h | 2 +-
t/t57xx-ssh-options-config.sh | 198 +++++++++++++++++++++++++++++++
transport.c | 9 +-
8 files changed, 375 insertions(+), 13 deletions(-)
create mode 100755 t/t57xx-ssh-options-config.sh
--
2.53.0.722.g8e572876c5
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH 1/3] connect: Rename name to command in connect_git() 2026-03-26 23:37 [PATCH 0/3] Add support for per-remote and per-namespace SSH options Wesley Schwengle @ 2026-03-26 23:37 ` Wesley Schwengle 2026-03-27 21:33 ` Jeff King 2026-03-26 23:37 ` [PATCH 2/3] connect: Add transport->remote->name to git_connect() Wesley Schwengle ` (2 subsequent siblings) 3 siblings, 1 reply; 22+ messages in thread From: Wesley Schwengle @ 2026-03-26 23:37 UTC (permalink / raw) To: git Cc: Junio C Hamano, Jiang Xin, Jeff King, Derrick Stolee, Patrick Steinhardt connect_git has `char *name' in its signature and it caught me a little offguard. I initially thought it was the remote name. But when you look closer at the various call sites it is actually a command that is send over the wire, eg . `git-receive-pack'. Change the naming makes it easier to read the code and understand its intention. Signed-off-by: Wesley Schwengle <wesleys@opperschaap.net> --- connect.c | 4 ++-- connect.h | 2 +- transport.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/connect.c b/connect.c index a02583a102..29af453b41 100644 --- a/connect.c +++ b/connect.c @@ -1420,35 +1420,35 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host, * does not need fork(2), or a struct child_process object if it does. Once * done, finish the connection with finish_connect() with the value returned * from this function (it is safe to call finish_connect() with NULL to * support the former case). * * If it returns, the connect is successful; it just dies on errors (this * will hopefully be changed in a libification effort, to return NULL when * the connection failed). */ struct child_process *git_connect(int fd[2], const char *url, - const char *name, + const char *command, const char *prog, int flags) { char *hostandport, *path; struct child_process *conn; enum protocol protocol; enum protocol_version version = get_protocol_version_config(); /* * NEEDSWORK: If we are trying to use protocol v2 and we are planning * to perform any operation that doesn't involve upload-pack (i.e., a * fetch, ls-remote, etc), then fallback to v0 since we don't know how * to do anything else (like push or remote archive) via v2. */ - if (version == protocol_v2 && strcmp("git-upload-pack", name)) + if (version == protocol_v2 && strcmp("git-upload-pack", command)) version = protocol_v0; /* Without this we cannot rely on waitpid() to tell * what happened to our children. */ signal(SIGCHLD, SIG_DFL); protocol = parse_connect_url(url, &hostandport, &path); if ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) { printf("Diag: url=%s\n", url ? url : "NULL"); diff --git a/connect.h b/connect.h index 1645126c17..f993626473 100644 --- a/connect.h +++ b/connect.h @@ -1,20 +1,20 @@ #ifndef CONNECT_H #define CONNECT_H #include "protocol.h" #define CONNECT_VERBOSE (1u << 0) #define CONNECT_DIAG_URL (1u << 1) #define CONNECT_IPV4 (1u << 2) #define CONNECT_IPV6 (1u << 3) -struct child_process *git_connect(int fd[2], const char *url, const char *name, const char *prog, int flags); +struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, int flags); int finish_connect(struct child_process *conn); int git_connection_is_socket(struct child_process *conn); int server_supports(const char *feature); int parse_feature_request(const char *features, const char *feature); const char *server_feature_value(const char *feature, size_t *len_ret); int url_is_local_not_ssh(const char *url); struct packet_reader; enum protocol_version discover_version(struct packet_reader *reader); diff --git a/transport.c b/transport.c index cb1befba8c..27a99190c0 100644 --- a/transport.c +++ b/transport.c @@ -949,26 +949,26 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re close(data->fd[1]); close(data->fd[0]); ret |= finish_connect(data->conn); data->conn = NULL; data->finished_handshake = 0; return ret; } -static int connect_git(struct transport *transport, const char *name, +static int connect_git(struct transport *transport, const char *command, const char *executable, int fd[2]) { struct git_transport_data *data = transport->data; data->conn = git_connect(data->fd, transport->url, - name, executable, 0); + command, executable, 0); fd[0] = data->fd[0]; fd[1] = data->fd[1]; return 0; } static int disconnect_git(struct transport *transport) { struct git_transport_data *data = transport->data; if (data->conn) { if (data->finished_handshake && !transport->stateless_rpc) -- 2.53.0.722.g8e572876c5 ^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 1/3] connect: Rename name to command in connect_git() 2026-03-26 23:37 ` [PATCH 1/3] connect: Rename name to command in connect_git() Wesley Schwengle @ 2026-03-27 21:33 ` Jeff King 2026-03-28 0:58 ` Wesley 0 siblings, 1 reply; 22+ messages in thread From: Jeff King @ 2026-03-27 21:33 UTC (permalink / raw) To: Wesley Schwengle Cc: git, Junio C Hamano, Jiang Xin, Derrick Stolee, Patrick Steinhardt On Thu, Mar 26, 2026 at 07:37:36PM -0400, Wesley Schwengle wrote: > connect_git has `char *name' in its signature and it caught me a little > offguard. I initially thought it was the remote name. But when you look > closer at the various call sites it is actually a command that is send > over the wire, eg . `git-receive-pack'. Change the naming makes it > easier to read the code and understand its intention. I agree that "name" is not all that descriptive, but I think there's a hidden gotcha in the explanation above. This string is _not_ the command that we send over the wire. That's "prog" in the same function. And the reason that "name" exists is that it is a stable name for the operation we are performing, like "git-receive-pack", even if configuration or command-line parameters (like "--receive-pack=foo") tell us to use a different command name. So probably "op" or "type" is a more accurate description. This conceptually ought to be an enum, too, since it is selecting from a limited set of operations we know about. I took a quick stab at converting it to an enum (see below) and it's mostly an improvement, but: 1. The ripple effect went much farther than I expected, since the transport code is passing these values, too. If we are going to update one function in the chain, we should probably do all of them (even if it is just a change of the variable name). 2. We end up having to convert to a string at some points anyway for producing error messages, and for passing across the remote-helper barrier. But I think we are still better off, because it's more clear where we are using the string-ified version and what values it could take. -Peff --- diff --git a/builtin/archive.c b/builtin/archive.c index 13ea7308c8..3c1288a123 100644 --- a/builtin/archive.c +++ b/builtin/archive.c @@ -31,7 +31,7 @@ static int run_remote_archiver(int argc, const char **argv, _remote = remote_get(remote); transport = transport_get(_remote, _remote->url.v[0]); - transport_connect(transport, "git-upload-archive", exec, fd); + transport_connect(transport, GIT_CONNECT_UPLOAD_ARCHIVE, exec, fd); /* * Inject a fake --format field at the beginning of the diff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c index d9e42bad58..316badd969 100644 --- a/builtin/fetch-pack.c +++ b/builtin/fetch-pack.c @@ -223,7 +223,7 @@ int cmd_fetch_pack(int argc, int flags = args.verbose ? CONNECT_VERBOSE : 0; if (args.diag_url) flags |= CONNECT_DIAG_URL; - conn = git_connect(fd, dest, "git-upload-pack", + conn = git_connect(fd, dest, GIT_CONNECT_UPLOAD_PACK, args.uploadpack, flags); if (!conn) return args.diag_url ? 0 : 1; diff --git a/builtin/send-pack.c b/builtin/send-pack.c index 8b81c8a848..1412b49bc8 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -273,8 +273,9 @@ int cmd_send_pack(int argc, fd[0] = 0; fd[1] = 1; } else { - conn = git_connect(fd, dest, "git-receive-pack", receivepack, - args.verbose ? CONNECT_VERBOSE : 0); + conn = git_connect(fd, dest, GIT_CONNECT_RECEIVE_PACK, + receivepack, + args.verbose ? CONNECT_VERBOSE : 0); } packet_reader_init(&reader, fd[0], NULL, 0, diff --git a/connect.c b/connect.c index a02583a102..dad1cff1a8 100644 --- a/connect.c +++ b/connect.c @@ -1428,6 +1428,7 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host, */ struct child_process *git_connect(int fd[2], const char *url, const char *name, + enum git_connect_type type, const char *prog, int flags) { char *hostandport, *path; @@ -1441,7 +1442,7 @@ struct child_process *git_connect(int fd[2], const char *url, * fetch, ls-remote, etc), then fallback to v0 since we don't know how * to do anything else (like push or remote archive) via v2. */ - if (version == protocol_v2 && strcmp("git-upload-pack", name)) + if (version == protocol_v2 && type != GIT_CONNECT_UPLOAD_PACK) version = protocol_v0; /* Without this we cannot rely on waitpid() to tell diff --git a/connect.h b/connect.h index 1645126c17..641498c759 100644 --- a/connect.h +++ b/connect.h @@ -7,7 +7,12 @@ #define CONNECT_DIAG_URL (1u << 1) #define CONNECT_IPV4 (1u << 2) #define CONNECT_IPV6 (1u << 3) -struct child_process *git_connect(int fd[2], const char *url, const char *name, const char *prog, int flags); +enum git_connect_type { + GIT_CONNECT_UPLOAD_PACK, + GIT_CONNECT_RECEIVE_PACK, + GIT_CONNECT_UPLOAD_ARCHIVE, +}; +struct child_process *git_connect(int fd[2], const char *url, enum git_connect_type, const char *prog, int flags); int finish_connect(struct child_process *conn); int git_connection_is_socket(struct child_process *conn); int server_supports(const char *feature); diff --git a/transport-helper.c b/transport-helper.c index 4d95d84f9e..c7fab6f560 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -620,8 +620,22 @@ static int run_connect(struct transport *transport, struct strbuf *cmdbuf) return ret; } +static const char *connect_type_to_command(enum git_connect_type type) +{ + switch (type) { + case GIT_CONNECT_UPLOAD_PACK: + return "git-upload-pack"; + case GIT_CONNECT_RECEIVE_PACK: + return "git-receive-pack"; + case GIT_CONNECT_UPLOAD_ARCHIVE: + return "git-upload-archive"; + } + BUG("unknown git_connect_type: %d", type); +} + static int process_connect_service(struct transport *transport, - const char *name, const char *exec) + enum git_connect_type type, + const char *exec) { struct helper_data *data = transport->data; struct strbuf cmdbuf = STRBUF_INIT; @@ -631,7 +645,7 @@ static int process_connect_service(struct transport *transport, * Handle --upload-pack and friends. This is fire and forget... * just warn if it fails. */ - if (strcmp(name, exec)) { + if (strcmp(connect_type_to_command(type), exec)) { int r = set_helper_option(transport, "servpath", exec); if (r > 0) warning(_("setting remote service path not supported by protocol")); @@ -640,13 +654,13 @@ static int process_connect_service(struct transport *transport, } if (data->connect) { - strbuf_addf(&cmdbuf, "connect %s\n", name); + strbuf_addf(&cmdbuf, "connect %s\n", connect_type_to_command(type)); ret = run_connect(transport, &cmdbuf); } else if (data->stateless_connect && (get_protocol_version_config() == protocol_v2) && - (!strcmp("git-upload-pack", name) || - !strcmp("git-upload-archive", name))) { - strbuf_addf(&cmdbuf, "stateless-connect %s\n", name); + (type == GIT_CONNECT_UPLOAD_PACK || + type == GIT_CONNECT_UPLOAD_ARCHIVE)) { + strbuf_addf(&cmdbuf, "stateless-connect %s\n", connect_type_to_command(type)); ret = run_connect(transport, &cmdbuf); if (ret) transport->stateless_rpc = 1; @@ -660,32 +674,33 @@ static int process_connect(struct transport *transport, int for_push) { struct helper_data *data = transport->data; - const char *name; + enum git_connect_type type; const char *exec; int ret; - name = for_push ? "git-receive-pack" : "git-upload-pack"; + type = for_push ? GIT_CONNECT_RECEIVE_PACK : GIT_CONNECT_UPLOAD_PACK; if (for_push) exec = data->transport_options.receivepack; else exec = data->transport_options.uploadpack; - ret = process_connect_service(transport, name, exec); + ret = process_connect_service(transport, type, exec); if (ret) do_take_over(transport); return ret; } -static int connect_helper(struct transport *transport, const char *name, - const char *exec, int fd[2]) +static int connect_helper(struct transport *transport, enum git_connect_type type, + const char *exec, int fd[2]) { struct helper_data *data = transport->data; /* Get_helper so connect is inited. */ get_helper(transport); - if (!process_connect_service(transport, name, exec)) - die(_("can't connect to subservice %s"), name); + if (!process_connect_service(transport, type, exec)) + die(_("can't connect to subservice %s"), + connect_type_to_command(type)); fd[0] = data->helper->out; fd[1] = data->helper->in; diff --git a/transport-internal.h b/transport-internal.h index 90ea749e5c..1a86c63ce0 100644 --- a/transport-internal.h +++ b/transport-internal.h @@ -58,7 +58,7 @@ struct transport_vtable { * process involved generating new commits. **/ int (*push_refs)(struct transport *transport, struct ref *refs, int flags); - int (*connect)(struct transport *connection, const char *name, + int (*connect)(struct transport *connection, enum git_connect_type type, const char *executable, int fd[2]); /** get_refs_list(), fetch(), and push_refs() can keep diff --git a/transport.c b/transport.c index cb1befba8c..2fd94d701f 100644 --- a/transport.c +++ b/transport.c @@ -308,8 +308,8 @@ static int connect_setup(struct transport *transport, int for_push) data->conn = git_connect(data->fd, transport->url, for_push ? - "git-receive-pack" : - "git-upload-pack", + GIT_CONNECT_RECEIVE_PACK : + GIT_CONNECT_UPLOAD_PACK, for_push ? data->options.receivepack : data->options.uploadpack, @@ -956,12 +956,12 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re return ret; } -static int connect_git(struct transport *transport, const char *name, +static int connect_git(struct transport *transport, enum git_connect_type type, const char *executable, int fd[2]) { struct git_transport_data *data = transport->data; data->conn = git_connect(data->fd, transport->url, - name, executable, 0); + type, executable, 0); fd[0] = data->fd[0]; fd[1] = data->fd[1]; return 0; @@ -1650,11 +1650,11 @@ void transport_unlock_pack(struct transport *transport, unsigned int flags) string_list_clear(&transport->pack_lockfiles, 0); } -int transport_connect(struct transport *transport, const char *name, +int transport_connect(struct transport *transport, enum git_connect_type type, const char *exec, int fd[2]) { if (transport->vtable->connect) - return transport->vtable->connect(transport, name, exec, fd); + return transport->vtable->connect(transport, type, exec, fd); else die(_("operation not supported by protocol")); } diff --git a/transport.h b/transport.h index 892f19454a..1e6fd263f6 100644 --- a/transport.h +++ b/transport.h @@ -5,6 +5,7 @@ #include "remote.h" #include "list-objects-filter-options.h" #include "string-list.h" +#include "connect.h" struct git_transport_options { unsigned thin : 1; @@ -324,7 +325,7 @@ char *transport_anonymize_url(const char *url); void transport_take_over(struct transport *transport, struct child_process *child); -int transport_connect(struct transport *transport, const char *name, +int transport_connect(struct transport *transport, enum git_connect_type type, const char *exec, int fd[2]); /* Transport methods defined outside transport.c */ ^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 1/3] connect: Rename name to command in connect_git() 2026-03-27 21:33 ` Jeff King @ 2026-03-28 0:58 ` Wesley 2026-03-28 1:44 ` Jeff King 0 siblings, 1 reply; 22+ messages in thread From: Wesley @ 2026-03-28 0:58 UTC (permalink / raw) To: Jeff King Cc: git, Junio C Hamano, Jiang Xin, Derrick Stolee, Patrick Steinhardt On 3/27/26 17:33, Jeff King wrote: > On Thu, Mar 26, 2026 at 07:37:36PM -0400, Wesley Schwengle wrote: > >> connect_git has `char *name' in its signature and it caught me a little >> offguard. I initially thought it was the remote name. But when you look >> closer at the various call sites it is actually a command that is send >> over the wire, eg . `git-receive-pack'. Change the naming makes it >> easier to read the code and understand its intention. > > I agree that "name" is not all that descriptive, but I think there's a > hidden gotcha in the explanation above. This string is _not_ the command > that we send over the wire. That's "prog" in the same function. And the > reason that "name" exists is that it is a stable name for the operation > we are performing, like "git-receive-pack", even if configuration or > command-line parameters (like "--receive-pack=foo") tell us to use a > different command name. > > So probably "op" or "type" is a more accurate description. This > conceptually ought to be an enum, too, since it is selecting from a > limited set of operations we know about. That's a fair take on it, "name" is really a not the best name for this variable. I think "op" covers what you describe here best, it reflects also why I named it command. When you check what is sent via ssh, it looks like the command: ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack 'waterkip/git.git' That's why in my change it was named command, op, or operation covers it too. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 1/3] connect: Rename name to command in connect_git() 2026-03-28 0:58 ` Wesley @ 2026-03-28 1:44 ` Jeff King 2026-03-28 2:01 ` Wesley 0 siblings, 1 reply; 22+ messages in thread From: Jeff King @ 2026-03-28 1:44 UTC (permalink / raw) To: Wesley; +Cc: git, Junio C Hamano, Jiang Xin, Derrick Stolee, Patrick Steinhardt On Fri, Mar 27, 2026 at 08:58:22PM -0400, Wesley wrote: > > So probably "op" or "type" is a more accurate description. This > > conceptually ought to be an enum, too, since it is selecting from a > > limited set of operations we know about. > > That's a fair take on it, "name" is really a not the best name for this > variable. I think "op" covers what you describe here best, it reflects also > why I named it command. When you check what is sent via ssh, it looks like > the command: > > ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack > 'waterkip/git.git' Right, but it's necessarily what is sent via ssh. E.g.: $ GIT_TRACE=1 git ls-remote example.com:repo.git [...] trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'git-upload-pack '\''repo.git'\''' $ GIT_TRACE=1 git ls-remote --upload-pack=foobar example.com:repo.git [...] trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'foobar '\''repo.git'\''' That's why I think "command" is actively misleading, because between "prog" and "command" it is not clear which one is going to be sent to the remote. -Peff ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 1/3] connect: Rename name to command in connect_git() 2026-03-28 1:44 ` Jeff King @ 2026-03-28 2:01 ` Wesley 0 siblings, 0 replies; 22+ messages in thread From: Wesley @ 2026-03-28 2:01 UTC (permalink / raw) To: Jeff King Cc: git, Junio C Hamano, Jiang Xin, Derrick Stolee, Patrick Steinhardt On 3/27/26 21:44, Jeff King wrote: > On Fri, Mar 27, 2026 at 08:58:22PM -0400, Wesley wrote: > >>> So probably "op" or "type" is a more accurate description. This >>> conceptually ought to be an enum, too, since it is selecting from a >>> limited set of operations we know about. >> >> That's a fair take on it, "name" is really a not the best name for this >> variable. I think "op" covers what you describe here best, it reflects also >> why I named it command. When you check what is sent via ssh, it looks like >> the command: >> >> ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack >> 'waterkip/git.git' > > Right, but it's necessarily what is sent via ssh. E.g.: > > $ GIT_TRACE=1 git ls-remote example.com:repo.git > [...] > trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'git-upload-pack '\''repo.git'\''' > > $ GIT_TRACE=1 git ls-remote --upload-pack=foobar example.com:repo.git > [...] > trace: start_command: /usr/bin/ssh -o SendEnv=GIT_PROTOCOL example.com 'foobar '\''repo.git'\''' > > That's why I think "command" is actively misleading, because between > "prog" and "command" it is not clear which one is going to be sent to > the remote. Ha! Interesting. I see the confusion :) I'm not really sure what to call it. I see the manpage calls it 'exec': --upload-pack=<exec> Specify the full path of git-upload-pack on the remote host. This allows listing references from repositories accessed via SSH and where the SSH daemon does not use the PATH configured by the user. and it's the full path of the git-upload-pack command if the remote doesn't use the PATH. So it is command, just.. I'm not sure what to call it. It executable, binary, program, operation, script. I feel they all cover the same concept. remote-command? It could be any of them iyam. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH 2/3] connect: Add transport->remote->name to git_connect() 2026-03-26 23:37 [PATCH 0/3] Add support for per-remote and per-namespace SSH options Wesley Schwengle 2026-03-26 23:37 ` [PATCH 1/3] connect: Rename name to command in connect_git() Wesley Schwengle @ 2026-03-26 23:37 ` Wesley Schwengle 2026-03-27 21:39 ` Jeff King 2026-03-26 23:37 ` [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options Wesley Schwengle 2026-03-27 7:51 ` [PATCH 0/3] " Johannes Sixt 3 siblings, 1 reply; 22+ messages in thread From: Wesley Schwengle @ 2026-03-26 23:37 UTC (permalink / raw) To: git; +Cc: Li Linchao, Junio C Hamano, Derrick Stolee, Jeff King To support `remote.$name.sshIdentityFile', and `remote.$name.sshOpts' for connecting to various remotes I need to pass around the remote down to git_connect. This commit introduces the `remote_name' and sprinkles all call sites to pass `NULL'. This is a non-breaking forward change Signed-off-by: Wesley Schwengle <wesleys@opperschaap.net> --- builtin/fetch-pack.c | 2 +- builtin/send-pack.c | 2 +- connect.c | 5 +++-- connect.h | 2 +- transport.c | 7 ++++++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c index d9e42bad58..f422acce06 100644 --- a/builtin/fetch-pack.c +++ b/builtin/fetch-pack.c @@ -217,21 +217,21 @@ int cmd_fetch_pack(int argc, if (args.stateless_rpc) { conn = NULL; fd[0] = 0; fd[1] = 1; } else { int flags = args.verbose ? CONNECT_VERBOSE : 0; if (args.diag_url) flags |= CONNECT_DIAG_URL; conn = git_connect(fd, dest, "git-upload-pack", - args.uploadpack, flags); + args.uploadpack, NULL, flags); if (!conn) return args.diag_url ? 0 : 1; } packet_reader_init(&reader, fd[0], NULL, 0, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_GENTLE_ON_EOF | PACKET_READ_DIE_ON_ERR_PACKET); version = discover_version(&reader); diff --git a/builtin/send-pack.c b/builtin/send-pack.c index 8b81c8a848..65efa91208 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -266,21 +266,21 @@ int cmd_send_pack(int argc, if (progress == -1) progress = !args.quiet && isatty(2); args.progress = progress; if (args.stateless_rpc) { conn = NULL; fd[0] = 0; fd[1] = 1; } else { - conn = git_connect(fd, dest, "git-receive-pack", receivepack, + conn = git_connect(fd, dest, "git-receive-pack", receivepack, NULL, args.verbose ? CONNECT_VERBOSE : 0); } packet_reader_init(&reader, fd[0], NULL, 0, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_GENTLE_ON_EOF | PACKET_READ_DIE_ON_ERR_PACKET); switch (discover_version(&reader)) { case protocol_v2: diff --git a/connect.c b/connect.c index 29af453b41..5749ddec9b 100644 --- a/connect.c +++ b/connect.c @@ -1420,22 +1420,22 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host, * does not need fork(2), or a struct child_process object if it does. Once * done, finish the connection with finish_connect() with the value returned * from this function (it is safe to call finish_connect() with NULL to * support the former case). * * If it returns, the connect is successful; it just dies on errors (this * will hopefully be changed in a libification effort, to return NULL when * the connection failed). */ struct child_process *git_connect(int fd[2], const char *url, - const char *command, - const char *prog, int flags) + const char *command, const char *prog, + const char *remote_name, int flags) { char *hostandport, *path; struct child_process *conn; enum protocol protocol; enum protocol_version version = get_protocol_version_config(); /* * NEEDSWORK: If we are trying to use protocol v2 and we are planning * to perform any operation that doesn't involve upload-pack (i.e., a * fetch, ls-remote, etc), then fallback to v0 since we don't know how @@ -1487,20 +1487,21 @@ struct child_process *git_connect(int fd[2], const char *url, if (!port) port = get_port(ssh_host); if (flags & CONNECT_DIAG_URL) { printf("Diag: url=%s\n", url ? url : "NULL"); printf("Diag: protocol=%s\n", prot_name(protocol)); printf("Diag: userandhost=%s\n", ssh_host ? ssh_host : "NULL"); printf("Diag: port=%s\n", port ? port : "NONE"); printf("Diag: path=%s\n", path ? path : "NULL"); + printf("Diag: remote=%s\n", remote_name ? remote_name : "NULL"); free(hostandport); free(path); child_process_clear(conn); free(conn); strbuf_release(&cmd); return NULL; } conn->trace2_child_class = "transport/ssh"; fill_ssh_args(conn, ssh_host, port, version, flags); diff --git a/connect.h b/connect.h index f993626473..ff54061e81 100644 --- a/connect.h +++ b/connect.h @@ -1,20 +1,20 @@ #ifndef CONNECT_H #define CONNECT_H #include "protocol.h" #define CONNECT_VERBOSE (1u << 0) #define CONNECT_DIAG_URL (1u << 1) #define CONNECT_IPV4 (1u << 2) #define CONNECT_IPV6 (1u << 3) -struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, int flags); +struct child_process *git_connect(int fd[2], const char *url, const char *command, const char *prog, const char *remote_name, int flags); int finish_connect(struct child_process *conn); int git_connection_is_socket(struct child_process *conn); int server_supports(const char *feature); int parse_feature_request(const char *features, const char *feature); const char *server_feature_value(const char *feature, size_t *len_ret); int url_is_local_not_ssh(const char *url); struct packet_reader; enum protocol_version discover_version(struct packet_reader *reader); diff --git a/transport.c b/transport.c index 27a99190c0..b9dcbf8d9e 100644 --- a/transport.c +++ b/transport.c @@ -289,37 +289,39 @@ static int set_git_option(struct git_transport_options *opts, opts->reject_shallow = !!value; return 0; } return 1; } static int connect_setup(struct transport *transport, int for_push) { struct git_transport_data *data = transport->data; int flags = transport->verbose > 0 ? CONNECT_VERBOSE : 0; + const char *remote_name = transport->remote->name; if (data->conn) return 0; switch (transport->family) { case TRANSPORT_FAMILY_ALL: break; case TRANSPORT_FAMILY_IPV4: flags |= CONNECT_IPV4; break; case TRANSPORT_FAMILY_IPV6: flags |= CONNECT_IPV6; break; } data->conn = git_connect(data->fd, transport->url, for_push ? "git-receive-pack" : "git-upload-pack", for_push ? data->options.receivepack : data->options.uploadpack, + remote_name, flags); return 0; } static void die_if_server_options(struct transport *transport) { if (!transport->server_options || !transport->server_options->nr) return; advise(_("see protocol.version in 'git help config' for more details")); @@ -953,22 +955,25 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re data->conn = NULL; data->finished_handshake = 0; return ret; } static int connect_git(struct transport *transport, const char *command, const char *executable, int fd[2]) { struct git_transport_data *data = transport->data; + const char *remote_name = transport->remote->name; + data->conn = git_connect(data->fd, transport->url, - command, executable, 0); + command, executable, remote_name, + 0); fd[0] = data->fd[0]; fd[1] = data->fd[1]; return 0; } static int disconnect_git(struct transport *transport) { struct git_transport_data *data = transport->data; if (data->conn) { if (data->finished_handshake && !transport->stateless_rpc) -- 2.53.0.722.g8e572876c5 ^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 2/3] connect: Add transport->remote->name to git_connect() 2026-03-26 23:37 ` [PATCH 2/3] connect: Add transport->remote->name to git_connect() Wesley Schwengle @ 2026-03-27 21:39 ` Jeff King 0 siblings, 0 replies; 22+ messages in thread From: Jeff King @ 2026-03-27 21:39 UTC (permalink / raw) To: Wesley Schwengle; +Cc: git, Li Linchao, Junio C Hamano, Derrick Stolee On Thu, Mar 26, 2026 at 07:37:37PM -0400, Wesley Schwengle wrote: > To support `remote.$name.sshIdentityFile', and `remote.$name.sshOpts' for > connecting to various remotes I need to pass around the remote down to > git_connect. This commit introduces the `remote_name' and sprinkles all > call sites to pass `NULL'. This is a non-breaking forward change My gut feeling is here is that this is going to be the wrong level for reading config, because it's too low and too coarse. If we ever want to have a command-line option for overriding config, like "git fetch --ssh-identity-file=foo", then how can the higher level git-fetch code pass down that single item? I.e., I think the ideal form of this would be that we pass around an ssh_options_context struct, high-level commands fill in that struct based on command-line options or config (including remote-specific ones), and then we act on it at the lowest level when spawning ssh. All that said, my first thought here is that most of what this series does is already possible with ssh config. It looks like that has already been suggested elsewhere in the thread, so I'll go read that before commenting further. -Peff ^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options 2026-03-26 23:37 [PATCH 0/3] Add support for per-remote and per-namespace SSH options Wesley Schwengle 2026-03-26 23:37 ` [PATCH 1/3] connect: Rename name to command in connect_git() Wesley Schwengle 2026-03-26 23:37 ` [PATCH 2/3] connect: Add transport->remote->name to git_connect() Wesley Schwengle @ 2026-03-26 23:37 ` Wesley Schwengle 2026-03-27 21:45 ` Jeff King 2026-03-27 7:51 ` [PATCH 0/3] " Johannes Sixt 3 siblings, 1 reply; 22+ messages in thread From: Wesley Schwengle @ 2026-03-26 23:37 UTC (permalink / raw) To: git Cc: Jeff King, Christian Couder, Junio C Hamano, Ævar Arnfjörð Bjarmason, Bence Ferdinandy Git relied on external SSH configuration (e.g. `~/.ssh/config')zR or wrapper scripts to select identity files and additional SSH options. This commit adds support for configuring SSH options directly in git config. Making it easier for users to select the correct identity at their respective forges. The following configuration is supported, in order of precedence: 1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts' 2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>' Where <owner> is derived from the repository path. Nested groups aren't supported: git@host:owner/repo.git becomes "owner", git@host:owner/group/repo.git also becomes "owner". 3. `core.sshIdentityFile' and `core.sshOpts' When `sshIdentityFile' is configured without `sshOpts', we inject `-F /dev/null' to prevent selecting additional identities from `~/.ssh/config'. If `sshOpts' are provided, it is used as-is and the user is responsible for specifying `-F /dev/null' if desired. This allows selecting SSH identities and options without relying on host aliases or wrapper scripts. Implementation details: * Introduce a connection context (cnx_context) to carry the remote name and repository owner. * Introduce ssh_options to encapsulate resolved SSH configuration. Limitations: * Separate push/pull URLs are not supported. * OpenSSH is the only supported ssh implemenation. Signed-off-by: Wesley Schwengle <wesleys@opperschaap.net> --- Documentation/config/core.adoc | 22 ++++ Documentation/config/remote.adoc | 9 ++ connect.c | 137 ++++++++++++++++++++- t/t57xx-ssh-options-config.sh | 198 +++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+), 5 deletions(-) create mode 100755 t/t57xx-ssh-options-config.sh diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index a0ebf03e2e..6a221bdf3b 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -263,20 +263,42 @@ specify that no proxy be used for a given domain pattern. This is useful for excluding servers inside a firewall from proxy use, while defaulting to a common proxy for external domains. core.sshCommand:: If this variable is set, `git fetch` and `git push` will use the specified command instead of `ssh` when they need to connect to a remote system. The command is in the same form as the `GIT_SSH_COMMAND` environment variable and is overridden when the environment variable is set. +core.sshIdentityFile:: + Default SSH identity file to use for SSH transports. When an + `sshIdentityFile` is used, git adds `-o IdentitiesOnly=yes` to the ssh + options by default. This feature currently only supports OpenSSH. + +core.sshOpts:: + Default additional options to pass to the SSH command. + When `sshIdentityFile` is configured without `sshOpts`, git adds `-F + /dev/null` to the SSH invocation. When `sshOpts` is configured, it is + used as-is. This feature currently only supports OpenSSH. + +core.sshIdentityFile.<owner>:: + SSH identity file to use for repositories whose path begins with + `<owner>`. For example, `git@host:owner/repo.git` uses `owner`. + Overrides `core.sshIdentityFile` when `core.sshIdentityFile.<owner>` + equals the owner. + +core.sshOpts.<owner>:: + Default additional SSH options for repositories whose path begins + with `<owner>`. + Overrides `core.sshOpts` when `core.sshOpts.<owner>` equals the owner. + core.ignoreStat:: If true, Git will avoid using lstat() calls to detect if files have changed by setting the "assume-unchanged" bit for those tracked files which it has updated identically in both the index and working tree. + When files are modified outside of Git, the user will need to stage the modified files explicitly (e.g. see 'Examples' section in linkgit:git-update-index[1]). Git will not normally detect changes to those files. + diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 91e46f66f5..b40e30eb41 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -113,10 +113,19 @@ remote.<name>.followRemoteHEAD:: The default value is "create", which will create `remotes/<name>/HEAD` if it exists on the remote, but not locally; this will not touch an already existing local reference. Setting it to "warn" will print a message if the remote has a different value than the local one; in case there is no local reference, it behaves like "create". A variant on "warn" is "warn-if-not-$branch", which behaves like "warn", but if `HEAD` on the remote is `$branch` it will be silent. Setting it to "always" will silently update `remotes/<name>/HEAD` to the value on the remote. Finally, setting it to "never" will never change or create the local reference. + +remote.<name>.sshIdentityFile:: + Path to the SSH identity file to use for this remote when connecting + over SSH. Overrides `core.sshIdentityFile` and + `core.sshIdentityFile.<owner>`. + +remote.<name>.sshOpts:: + Additional options to pass to the SSH command for this remote. + Overrides `core.sshOpts` and `core.sshOpts.<owner>`. diff --git a/connect.c b/connect.c index 5749ddec9b..d185c1679a 100644 --- a/connect.c +++ b/connect.c @@ -21,20 +21,30 @@ #include "version.h" #include "protocol.h" #include "alias.h" #include "bundle-uri.h" #include "promisor-remote.h" static char *server_capabilities_v1; static struct strvec server_capabilities_v2 = STRVEC_INIT; static const char *next_server_feature_value(const char *feature, size_t *len, size_t *offset); +struct cnx_context { + char *owner; + const char *remote_name; +}; + +struct ssh_options { + const char *identity_file; + struct strvec ssh_opts; +}; + static int check_ref(const char *name, unsigned int flags) { if (!flags) return 1; if (!skip_prefix(name, "refs/", &name)) return 0; /* REF_NORMAL means that we don't want the magic fake tag refs */ if ((flags & REF_NORMAL) && check_refname_format(name, @@ -1295,34 +1305,140 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport, version, '\0'); } packet_write(fd[1], request.buf, request.len); free(target_host); strbuf_release(&request); return conn; } +static const char *get_ssh_config_values(struct cnx_context context, + const char *lookup) { + struct strbuf key = STRBUF_INIT; + const char *value = NULL; + + if (context.remote_name) { + strbuf_addf(&key, "remote.%s.%s", context.remote_name, lookup); + if (!repo_config_get_string_tmp(the_repository, key.buf, &value)) { + strbuf_release(&key); + return value; + } + strbuf_reset(&key); + } + if (context.owner) { + strbuf_addf(&key, "core.%s.%s", lookup, context.owner); + if (!repo_config_get_string_tmp(the_repository, key.buf, &value)) { + strbuf_release(&key); + return value; + } + strbuf_reset(&key); + } + strbuf_addf(&key, "core.%s", lookup); + if (!repo_config_get_string_tmp(the_repository, key.buf, &value)) { + strbuf_release(&key); + return value; + } + + strbuf_release(&key); + return NULL; +} + +/* + * Returns the first path component of `path`, which the caller must free(). + * Returns NULL if `path` is NULL or has no '/' separator. + */ +static char *repo_namespace(const char *path) +{ + const char *slash; + + if (!path) + return NULL; + + while (*path == '/') + path++; + + slash = strchr(path, '/'); + if (!slash) + return NULL; + + return xstrndup(path, slash - path); +} + +static struct ssh_options *get_ssh_options(struct cnx_context context) +{ + struct ssh_options *opts = xcalloc(1, sizeof(*opts)); + const char *sshopts; + strvec_init(&opts->ssh_opts); + + opts->identity_file = get_ssh_config_values(context, + "sshIdentityFile"); + + sshopts = get_ssh_config_values(context, "sshOpts"); + + if (sshopts) { + const char **argv = NULL; + char *cmdline = xstrdup(sshopts); + int argc = split_cmdline(cmdline, &argv); + int i; + + if (argc < 0) + die(_("bad sshOpts value: '%s'"), sshopts); + + for (i = 0; i < argc; i++) + strvec_push(&opts->ssh_opts, argv[i]); + + free((void *)argv); + free(cmdline); + } + + return opts; +} + +static void clear_ssh_options(struct ssh_options *opts) +{ + strvec_clear(&opts->ssh_opts); + free(opts); +} + /* * Append the appropriate environment variables to `env` and options to * `args` for running ssh in Git's SSH-tunneled transport. */ static void push_ssh_options(struct strvec *args, struct strvec *env, enum ssh_variant variant, const char *port, - enum protocol_version version, int flags) + enum protocol_version version, + struct ssh_options *ssh_options, int flags) { if (variant == VARIANT_SSH && version > 0) { strvec_push(args, "-o"); strvec_push(args, "SendEnv=" GIT_PROTOCOL_ENVIRONMENT); strvec_pushf(env, GIT_PROTOCOL_ENVIRONMENT "=version=%d", version); + + } + if (variant == VARIANT_SSH) { + if (ssh_options->identity_file) { + strvec_push(args, "-i"); + strvec_push(args, ssh_options->identity_file); + strvec_push(args, "-o"); + strvec_push(args, "IdentitiesOnly=yes"); + } + + if (ssh_options->identity_file && !ssh_options->ssh_opts.nr) { + strvec_push(args, "-F"); + strvec_push(args, "/dev/null"); + } + if (ssh_options->ssh_opts.nr > 0) { + strvec_pushv(args, ssh_options->ssh_opts.v); + } } if (flags & CONNECT_IPV4) { switch (variant) { case VARIANT_AUTO: BUG("VARIANT_AUTO passed to push_ssh_options"); case VARIANT_SIMPLE: die(_("ssh variant 'simple' does not support -4")); case VARIANT_SSH: case VARIANT_PLINK: @@ -1362,21 +1478,21 @@ static void push_ssh_options(struct strvec *args, struct strvec *env, strvec_push(args, "-P"); } strvec_push(args, port); } } /* Prepare a child_process for use by Git's SSH-tunneled transport. */ static void fill_ssh_args(struct child_process *conn, const char *ssh_host, const char *port, enum protocol_version version, - int flags) + struct ssh_options *ssh_options, int flags) { const char *ssh; enum ssh_variant variant; if (looks_like_command_line_option(ssh_host)) die(_("strange hostname '%s' blocked"), ssh_host); ssh = get_ssh_command(); if (ssh) { variant = determine_ssh_variant(ssh, 1); @@ -1396,29 +1512,29 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host, if (variant == VARIANT_AUTO) { struct child_process detect = CHILD_PROCESS_INIT; detect.use_shell = conn->use_shell; detect.no_stdin = detect.no_stdout = detect.no_stderr = 1; strvec_push(&detect.args, ssh); strvec_push(&detect.args, "-G"); push_ssh_options(&detect.args, &detect.env, - VARIANT_SSH, port, version, flags); + VARIANT_SSH, port, version, ssh_options, flags); strvec_push(&detect.args, ssh_host); variant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH; } strvec_push(&conn->args, ssh); push_ssh_options(&conn->args, &conn->env, variant, port, version, - flags); + ssh_options, flags); strvec_push(&conn->args, ssh_host); } /* * This returns the dummy child_process `no_fork` if the transport protocol * does not need fork(2), or a struct child_process object if it does. Once * done, finish the connection with finish_connect() with the value returned * from this function (it is safe to call finish_connect() with NULL to * support the former case). * @@ -1475,20 +1591,22 @@ struct child_process *git_connect(int fd[2], const char *url, /* remove repo-local variables from the environment */ for (var = local_repo_env; *var; var++) strvec_push(&conn->env, *var); conn->use_shell = 1; conn->in = conn->out = -1; if (protocol == PROTO_SSH) { char *ssh_host = hostandport; const char *port = NULL; + struct ssh_options *ssh_options; + struct cnx_context context; transport_check_allowed("ssh"); get_host_and_port(&ssh_host, &port); if (!port) port = get_port(ssh_host); if (flags & CONNECT_DIAG_URL) { printf("Diag: url=%s\n", url ? url : "NULL"); printf("Diag: protocol=%s\n", prot_name(protocol)); printf("Diag: userandhost=%s\n", ssh_host ? ssh_host : "NULL"); @@ -1496,22 +1614,31 @@ struct child_process *git_connect(int fd[2], const char *url, printf("Diag: path=%s\n", path ? path : "NULL"); printf("Diag: remote=%s\n", remote_name ? remote_name : "NULL"); free(hostandport); free(path); child_process_clear(conn); free(conn); strbuf_release(&cmd); return NULL; } + + context.owner = repo_namespace(path); + context.remote_name = remote_name; + ssh_options = get_ssh_options(context); + conn->trace2_child_class = "transport/ssh"; - fill_ssh_args(conn, ssh_host, port, version, flags); + fill_ssh_args(conn, ssh_host, port, version, + ssh_options, flags); + + clear_ssh_options(ssh_options); + free(context.owner); } else { transport_check_allowed("file"); conn->trace2_child_class = "transport/file"; if (version > 0) { strvec_pushf(&conn->env, GIT_PROTOCOL_ENVIRONMENT "=version=%d", version); } } strvec_push(&conn->args, cmd.buf); diff --git a/t/t57xx-ssh-options-config.sh b/t/t57xx-ssh-options-config.sh new file mode 100755 index 0000000000..6db5c3fa0b --- /dev/null +++ b/t/t57xx-ssh-options-config.sh @@ -0,0 +1,198 @@ +#!/bin/sh + +test_description='test git ssh options patch' + +. ./test-lib.sh + +write_script fake-ssh <<-\EOF && +echo "ssh: $*" >"$TRASH_DIRECTORY/ssh-output" +exit 0 +EOF + +test_expect_success 'setup ssh wrapper' ' + GIT_SSH="$PWD/fake-ssh" && + export GIT_SSH && + GIT_SSH_VARIANT=ssh && + export GIT_SSH_VARIANT && + export TRASH_DIRECTORY && + >"$TRASH_DIRECTORY"/ssh-output +' + +test_expect_success 'add remote' ' + git remote add origin git@myhost:owner/repo.git +' + +test_expect_success 'create branch' ' + git commit -m "Empty commit" --allow-empty && \ + git branch foo +' + +expect_ssh () { + test_when_finished '(cd "$TRASH_DIRECTORY" && rm -f ssh-expect && >ssh-output)' && + echo "ssh: $@" >"$TRASH_DIRECTORY/ssh-expect" && + (cd "$TRASH_DIRECTORY" && test_cmp ssh-expect ssh-output) +} + +test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch-pack' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git fetch-pack git@myhost:owner/repo.git && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch-pack and ssh:// uri' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git fetch-pack ssh://git@myhost/owner/repo.git && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-upload-pack '\''/owner/repo.git'\''" +' + +test_expect_success 'core sshIdentityFile is passed to ssh w/ send-pack' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git send-pack git@myhost:owner/repo.git && + expect_ssh -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshIdentityFile is passed to ssh w/ ls-remote' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git ls-remote && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshIdentityFile is passed to ssh w/ fetch' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git fetch && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshIdentityFile is passed to ssh w/ push' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_must_fail git push origin foo && + expect_ssh -i /.ssh/id_test \ + -o IdentitiesOnly=yes -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ fetch-pack' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git fetch-pack git@myhost:owner/repo.git && + expect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ fetch-pack and ssh:// uri' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git fetch-pack ssh://git@myhost/owner/repo.git && + expect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \ + "git-upload-pack '\''/owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ send-pack' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git send-pack git@myhost:owner/repo.git && + expect_ssh -v -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ ls-remote' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git ls-remote && + expect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ fetch' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git fetch && + expect_ssh -o SendEnv=GIT_PROTOCOL -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'core sshOpts is passed to ssh w/ push' ' + test_config core.sshOpts "-v -F /dev/null" && + test_must_fail git push origin foo && + expect_ssh -v -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'owner overrides core on fetch-pack' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_config core.sshIdentityFile.owner /.ssh/id_test_owner && + test_config core.sshOpts "-v" && + test_config core.sshOpts.owner "-v -F /dev/null" && + test_must_fail git fetch-pack git@myhost:owner/repo.git && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_owner \ + -o IdentitiesOnly=yes -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'owner overrides core on send-pack' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_config core.sshIdentityFile.owner /.ssh/id_test_owner && + test_config core.sshOpts "-v" && + test_config core.sshOpts.owner "-v -F /dev/null" && + test_must_fail git send-pack git@myhost:owner/repo.git && + expect_ssh -i /.ssh/id_test_owner -o IdentitiesOnly=yes \ + -v -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'remote overrides core on ls-remote' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_config remote.origin.sshIdentityFile /.ssh/id_test_remote && + test_config core.sshOpts "-v" && + test_config remote.origin.sshOpts "-v -F /dev/null" && + test_must_fail git ls-remote && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_remote \ + -o IdentitiesOnly=yes -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'remote overrides core on fetch' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_config remote.origin.sshIdentityFile /.ssh/id_test_remote && + test_config core.sshOpts "-v" && + test_config remote.origin.sshOpts "-v -F /dev/null" && + test_must_fail git fetch && + expect_ssh -o SendEnv=GIT_PROTOCOL -i /.ssh/id_test_remote \ + -o IdentitiesOnly=yes -v -F /dev/null git@myhost \ + "git-upload-pack '\''owner/repo.git'\''" +' + +test_expect_success 'remote overrides core on push' ' + test_config core.sshIdentityFile /.ssh/id_test && + test_config remote.origin.sshIdentityFile /.ssh/id_test_remote && + test_config core.sshOpts "-v" && + test_config remote.origin.sshOpts "-v -F /dev/null" && + test_must_fail git push origin foo && + expect_ssh -i /.ssh/id_test_remote -o IdentitiesOnly=yes \ + -v -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success "remote no SSH identity file or sshOpts are not injected" ' + test_must_fail git push origin foo && + expect_ssh git@myhost "git-receive-pack '\''owner/repo.git'\''" +' + +test_expect_success 'remote overrides owner on push' ' + test_config core.sshIdentityFile.owner /.ssh/id_test && + test_config remote.origin.sshIdentityFile /.ssh/id_test_remote && + test_config core.sshOpts.owner "-v" && + test_config remote.origin.sshOpts "-v -F /dev/null" && + test_must_fail git push origin foo && + expect_ssh -i /.ssh/id_test_remote -o IdentitiesOnly=yes \ + -v -F /dev/null git@myhost \ + "git-receive-pack '\''owner/repo.git'\''" +' + + +test_done -- 2.53.0.722.g8e572876c5 ^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options 2026-03-26 23:37 ` [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options Wesley Schwengle @ 2026-03-27 21:45 ` Jeff King 2026-03-28 0:43 ` Wesley 0 siblings, 1 reply; 22+ messages in thread From: Jeff King @ 2026-03-27 21:45 UTC (permalink / raw) To: Wesley Schwengle Cc: git, Christian Couder, Junio C Hamano, Ævar Arnfjörð Bjarmason, Bence Ferdinandy On Thu, Mar 26, 2026 at 07:37:38PM -0400, Wesley Schwengle wrote: > The following configuration is supported, in order of precedence: > > 1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts' > > 2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>' > > Where <owner> is derived from the repository path. Nested groups > aren't supported: git@host:owner/repo.git becomes "owner", > git@host:owner/group/repo.git also becomes "owner". We already have some conditional config mechanisms, and I don't think it's a good idea to add one that only works for certain keys. If I understand correctly, this <owner> feature can already be accomplished with: [includeIf "hasconfig:remote.*.url:**/owner/**"] path = all-your-options-for-that-owner It's a little more verbose (and you have to use a separate file), but it also allows other conditions, like "gitdir:" for selecting based on how you lay out your repos locally. -Peff ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options 2026-03-27 21:45 ` Jeff King @ 2026-03-28 0:43 ` Wesley 2026-03-28 2:03 ` Jeff King 0 siblings, 1 reply; 22+ messages in thread From: Wesley @ 2026-03-28 0:43 UTC (permalink / raw) To: Jeff King Cc: git, Christian Couder, Junio C Hamano, Ævar Arnfjörð Bjarmason, Bence Ferdinandy On 3/27/26 17:45, Jeff King wrote: > On Thu, Mar 26, 2026 at 07:37:38PM -0400, Wesley Schwengle wrote: > >> The following configuration is supported, in order of precedence: >> >> 1. `remote.<name>.sshIdentityFile' and `remote.<name>.sshOpts' >> >> 2. `core.sshIdentityFile.<owner>' and `core.sshOpts.<owner>' >> >> Where <owner> is derived from the repository path. Nested groups >> aren't supported: git@host:owner/repo.git becomes "owner", >> git@host:owner/group/repo.git also becomes "owner". > > We already have some conditional config mechanisms, and I don't think > it's a good idea to add one that only works for certain keys. If I > understand correctly, this <owner> feature can already be accomplished > with: > > [includeIf "hasconfig:remote.*.url:**/owner/**"] > path = all-your-options-for-that-owner > > It's a little more verbose (and you have to use a separate file), but it > also allows other conditions, like "gitdir:" for selecting based on how > you lay out your repos locally. This doesn't work as you would think it does. The includeIf on hasconfig with the remote URL is used if it finds the remote in the config, and not on the actual network action. Thus if you have two remotes with two includeIfs on the remote URL it takes the config of the last defined include. Thus breaks the expectation that it is configured. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options 2026-03-28 0:43 ` Wesley @ 2026-03-28 2:03 ` Jeff King 2026-03-28 2:25 ` Wesley 0 siblings, 1 reply; 22+ messages in thread From: Jeff King @ 2026-03-28 2:03 UTC (permalink / raw) To: Wesley Cc: git, Christian Couder, Junio C Hamano, Ævar Arnfjörð Bjarmason, Bence Ferdinandy On Fri, Mar 27, 2026 at 08:43:07PM -0400, Wesley wrote: > > We already have some conditional config mechanisms, and I don't think > > it's a good idea to add one that only works for certain keys. If I > > understand correctly, this <owner> feature can already be accomplished > > with: > > > > [includeIf "hasconfig:remote.*.url:**/owner/**"] > > path = all-your-options-for-that-owner > > > > It's a little more verbose (and you have to use a separate file), but it > > also allows other conditions, like "gitdir:" for selecting based on how > > you lay out your repos locally. > > This doesn't work as you would think it does. The includeIf on hasconfig > with the remote URL is used if it finds the remote in the config, and not on > the actual network action. Thus if you have two remotes with two includeIfs > on the remote URL it takes the config of the last defined include. Thus > breaks the expectation that it is configured. Yes, it's going to be per-local-repo. I had assumed you were in a situation where you were defining these setups at the global level, and each local repo will want to use them or not. I.e., something like this: [set up once] $ git config -f ~/.gitconfig-foo core.sshCommand "ssh -i whatever" $ git config --global includeIf.hasconfig:remote.*.url:example.com:foo/**.path .gitconfig-foo [and now we'd use it in this repo] $ git clone example.com:foo/repo.git [but not this one] $ git clone example.com:bar/repo.git If you have remotes for both "foo/repo.git" and "bar/repo.git" configured in one local repo, then yes, it will always apply the config. If you really want per-connection config, I'm still not quite convinced that you aren't better off defining host sections in your ssh config. That covers all options that ssh knows about (not just ones we teach Git about), and you can still apply it automatically from ~/.gitconfig using insteadOf. Something like: git config --global foo.example.com:foo/.insteadOf example.com:foo/ and then defining a foo.example.com block in your ~/.ssh/config. -Peff ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options 2026-03-28 2:03 ` Jeff King @ 2026-03-28 2:25 ` Wesley 0 siblings, 0 replies; 22+ messages in thread From: Wesley @ 2026-03-28 2:25 UTC (permalink / raw) To: Jeff King Cc: git, Christian Couder, Junio C Hamano, Ævar Arnfjörð Bjarmason, Bence Ferdinandy On 3/27/26 22:03, Jeff King wrote: > On Fri, Mar 27, 2026 at 08:43:07PM -0400, Wesley wrote: > igured in one local repo, then yes, it will always apply the config. > > If you really want per-connection config, I'm still not quite convinced > that you aren't better off defining host sections in your ssh config. > That covers all options that ssh knows about (not just ones we teach Git > about), and you can still apply it automatically from ~/.gitconfig using > insteadOf. Something like: > > git config --global foo.example.com:foo/.insteadOf example.com:foo/ > > and then defining a foo.example.com block in your ~/.ssh/config. This is where it breaks in my mind. I'm configuring ssh to configure git. Btw, I'm assuming you meant: git config --global url.foo.example.com:foo/.insteadOf example.com:foo/ I never took this approach with ssh identity files. I'll have a look at this approach see how it works. The submitted patch approach has served me over a number of years, albeit not directly in C. I just store the config in git and I don't need to worry about ssh anymore. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-26 23:37 [PATCH 0/3] Add support for per-remote and per-namespace SSH options Wesley Schwengle ` (2 preceding siblings ...) 2026-03-26 23:37 ` [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options Wesley Schwengle @ 2026-03-27 7:51 ` Johannes Sixt 2026-03-27 15:04 ` Wesley 2026-03-27 16:10 ` Junio C Hamano 3 siblings, 2 replies; 22+ messages in thread From: Johannes Sixt @ 2026-03-27 7:51 UTC (permalink / raw) To: Wesley Schwengle; +Cc: git Am 27.03.26 um 00:37 schrieb Wesley Schwengle: > * `remote.*.sshIdentityFile' and `remote.*.sshOpts' > > Configuration set on owner/path style. This is to support `includeIf` > configuration management. For example, a git-forge that host both > employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and > `git@gitlab.com/corp/git.git' would have something configured as: > > * `core.sshIdentityFile.*', eg > > [core "sshIdentityFile"] > waterkip = ~/.ssh/id_ed25519_me > corp = ~/.ssh/id_ed25519_corporate This can be solved without a changing Git today. You configure the two remotes with different fake host names: [remote "waterkip"] url = git@waterkip.gitlab/waterkip/git.git [remote "corp"] url = git@corp.gitlab/corp/git.git And set up the real host name and identity file in ~/.ssh/config: Host waterkip.gitlab IdentityFile ~/.ssh/id_ed25519_me HostName gitlab.com Host corp.gitlab IdentityFile ~/.ssh/id_ed25519_corporate HostName gitlab.com For this reason, I see little incentive to add complexity to Git that achieves the same. -- Hannes ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 7:51 ` [PATCH 0/3] " Johannes Sixt @ 2026-03-27 15:04 ` Wesley 2026-03-27 16:10 ` Junio C Hamano 1 sibling, 0 replies; 22+ messages in thread From: Wesley @ 2026-03-27 15:04 UTC (permalink / raw) To: Johannes Sixt; +Cc: Git maillinglist On 3/27/26 03:51, Johannes Sixt wrote: > Am 27.03.26 um 00:37 schrieb Wesley Schwengle: >> * `remote.*.sshIdentityFile' and `remote.*.sshOpts' >> >> Configuration set on owner/path style. This is to support `includeIf` >> configuration management. For example, a git-forge that host both >> employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and >> `git@gitlab.com/corp/git.git' would have something configured as: >> >> * `core.sshIdentityFile.*', eg >> >> [core "sshIdentityFile"] >> waterkip = ~/.ssh/id_ed25519_me >> corp = ~/.ssh/id_ed25519_corporate > For this reason, I see little incentive to add complexity to Git that > achieves the same. It's a hacky solution where you change the ssh configuration permanently. It breaks copy/paste(s) etc for every forge. In addition, this is where my need came from: It breaks myrepo's configuration(s) for people if they have to override the hostname in each myrepos config. You cannot simply override the hostname in these situtations because of a local ssh config change. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 7:51 ` [PATCH 0/3] " Johannes Sixt 2026-03-27 15:04 ` Wesley @ 2026-03-27 16:10 ` Junio C Hamano 2026-03-27 16:49 ` Wesley 2026-03-27 21:51 ` brian m. carlson 1 sibling, 2 replies; 22+ messages in thread From: Junio C Hamano @ 2026-03-27 16:10 UTC (permalink / raw) To: Johannes Sixt; +Cc: Wesley Schwengle, git Johannes Sixt <j6t@kdbg.org> writes: > Am 27.03.26 um 00:37 schrieb Wesley Schwengle: >> * `remote.*.sshIdentityFile' and `remote.*.sshOpts' >> >> Configuration set on owner/path style. This is to support `includeIf` >> configuration management. For example, a git-forge that host both >> employer/client repo's. Eg, `git@gitlab.com/waterkip/git.git' and >> `git@gitlab.com/corp/git.git' would have something configured as: >> >> * `core.sshIdentityFile.*', eg >> >> [core "sshIdentityFile"] >> waterkip = ~/.ssh/id_ed25519_me >> corp = ~/.ssh/id_ed25519_corporate > > This can be solved without a changing Git today. You configure the two > remotes with different fake host names: > > [remote "waterkip"] > url = git@waterkip.gitlab/waterkip/git.git > [remote "corp"] > url = git@corp.gitlab/corp/git.git > And set up the real host name and identity file in ~/.ssh/config: > > Host waterkip.gitlab > IdentityFile ~/.ssh/id_ed25519_me > HostName gitlab.com > > Host corp.gitlab > IdentityFile ~/.ssh/id_ed25519_corporate > HostName gitlab.com > > > For this reason, I see little incentive to add complexity to Git that > achieves the same. Very well said. I somehow thought that this practice is so widespread that it was one of the few first things any new people learn to do, but perhaps we do not have a good documentation coverage? In any case, I do not think these network/transport specific configuration would hardly belong to "core". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 16:10 ` Junio C Hamano @ 2026-03-27 16:49 ` Wesley 2026-03-27 22:06 ` brian m. carlson 2026-03-28 7:46 ` Johannes Sixt 2026-03-27 21:51 ` brian m. carlson 1 sibling, 2 replies; 22+ messages in thread From: Wesley @ 2026-03-27 16:49 UTC (permalink / raw) To: Junio C Hamano; +Cc: git, Johannes Sixt On 3/27/26 12:10, Junio C Hamano wrote: > I somehow thought that this practice is so widespread that it was > one of the few first things any new people learn to do, but perhaps > we do not have a good documentation coverage? As said before it is weird thing to configure a global ssh configuration just for git transport. It doesn't make much sense. The problem with ssh_config usage is that you need to change your ssh config, which is machine global, not just git. And not portable across teams with configurations committed to git. Myrepos is a good example of this. My former employer had this and I know the Perl metacpan project also uses mysrepos. Changing every URL dynamically in committed configs isn't really a nice ask. The alternative is using core.sshCommand to inject the correct keys, but you must apply logic there when you have multiple accounts or forges. Which is what I initially did with a zsh-scripts. Which is why I ported that logic to git itself, I thought it would be beneficial to have an easy way to maintain sshIdentityFile settings. In addition, for core.sshCommand to work you must use the full openssh command rather than just adding some options to it. Which is an added benefit of the proposed changes. This change makes key selection possible without too much trouble on the users side with hacks to ssh_config. You can just tell git to use an identity based on the remote. Solve a git identify problem in the git config, fix the problem in the correct domain. We also store email credentials in gitconfigs, why would an ssh identify file be treated different? > In any case, I do not think these network/transport specific > configuration would hardly belong to "core". I'm happy to move it elsewhere, as said, I chose core because core.sshCommand. As for the name: "ssh" or "transport", I'm not certain what is the best option is. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 16:49 ` Wesley @ 2026-03-27 22:06 ` brian m. carlson 2026-03-28 1:02 ` Wesley 2026-03-28 7:46 ` Johannes Sixt 1 sibling, 1 reply; 22+ messages in thread From: brian m. carlson @ 2026-03-27 22:06 UTC (permalink / raw) To: Wesley; +Cc: Junio C Hamano, git, Johannes Sixt [-- Attachment #1: Type: text/plain, Size: 2199 bytes --] On 2026-03-27 at 16:49:35, Wesley wrote: > On 3/27/26 12:10, Junio C Hamano wrote: > > > I somehow thought that this practice is so widespread that it was > > one of the few first things any new people learn to do, but perhaps > > we do not have a good documentation coverage? > > As said before it is weird thing to configure a global ssh configuration > just for git transport. It doesn't make much sense. > > The problem with ssh_config usage is that you need to change your ssh > config, which is machine global, not just git. And not portable across teams > with configurations committed to git. Myrepos is a good example of this. My > former employer had this and I know the Perl metacpan project also uses > mysrepos. Changing every URL dynamically in committed configs isn't really a > nice ask. You can also use the conditional inclusion functionality to rewrite URLs for repositories in a certain directory with `url.<URL>.insteadOf`. Or you can use conditional inclusion to use `core.sshCommand` with the `-i` option set appropriately. > The alternative is using core.sshCommand to inject the correct keys, but you > must apply logic there when you have multiple accounts or forges. Which is > what I initially did with a zsh-scripts. > Which is why I ported that logic to git itself, I thought it would be > beneficial to have an easy way to maintain sshIdentityFile settings. > > In addition, for core.sshCommand to work you must use the full openssh > command rather than just adding some options to it. Which is an added > benefit of the proposed changes. Right, but the additional burden is typing "ssh -i" for that option. That's not very substantial. And the existing option is much more flexible as well, since it allows you to use other options, such as `-o ControlMaster`, which is useful when you're using a security key and don't want to re-authenticate all the time. It also allows you to use arbitrary shell scripting, too, which means that you can customize the configuration depending on what keys are available or what machine you're on (or really anything else). -- brian m. carlson (they/them) Toronto, Ontario, CA [-- Attachment #2: signature.asc --] [-- Type: application/pgp-signature, Size: 325 bytes --] ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 22:06 ` brian m. carlson @ 2026-03-28 1:02 ` Wesley 0 siblings, 0 replies; 22+ messages in thread From: Wesley @ 2026-03-28 1:02 UTC (permalink / raw) To: brian m. carlson, Junio C Hamano, git, Johannes Sixt On 3/27/26 18:06, brian m. carlson wrote: > On 2026-03-27 at 16:49:35, Wesley wrote: >> On 3/27/26 12:10, Junio C Hamano wrote: >> >>> I somehow thought that this practice is so widespread that it was >>> one of the few first things any new people learn to do, but perhaps >>> we do not have a good documentation coverage? >> >> As said before it is weird thing to configure a global ssh configuration >> just for git transport. It doesn't make much sense. >> >> The problem with ssh_config usage is that you need to change your ssh >> config, which is machine global, not just git. And not portable across teams >> with configurations committed to git. Myrepos is a good example of this. My >> former employer had this and I know the Perl metacpan project also uses >> mysrepos. Changing every URL dynamically in committed configs isn't really a >> nice ask. > > You can also use the conditional inclusion functionality to rewrite URLs > for repositories in a certain directory with `url.<URL>.insteadOf`. Or > you can use conditional inclusion to use `core.sshCommand` with the `-i` > option set appropriately. That is what I did and why I thought a simple addition in git would make it declarative in git via its configuration. This would limit the scripting side for just adding "this identityFile should be used in this repo" or "this remote". Including allowing setting sshOpts for specific remotes and/or repos. Cheers, Wesley -- Wesley Why not both? ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 16:49 ` Wesley 2026-03-27 22:06 ` brian m. carlson @ 2026-03-28 7:46 ` Johannes Sixt 1 sibling, 0 replies; 22+ messages in thread From: Johannes Sixt @ 2026-03-28 7:46 UTC (permalink / raw) To: Wesley; +Cc: git, Junio C Hamano Am 27.03.26 um 17:49 schrieb Wesley: > On 3/27/26 12:10, Junio C Hamano wrote: >> I somehow thought that this practice is so widespread that it was >> one of the few first things any new people learn to do, but perhaps >> we do not have a good documentation coverage? > > As said before it is weird thing to configure a global ssh configuration > just for git transport. It doesn't make much sense. > > The problem with ssh_config usage is that you need to change your ssh > config, which is machine global, not just git. Are thinking about the SSH configuration in /etc/ssh? You do not have to change that. There is also a .ssh/config in the user's home directory. That configuration isn't machine global, it's obviously per user. And the way to make the configuration work only for Git is precisely to use fake host names that are only used in remote URLs of Git repositories. > And not portable across > teams with configurations committed to git. Myrepos is a good example of > this. My former employer had this and I know the Perl metacpan project > also uses mysrepos. Changing every URL dynamically in committed configs > isn't really a nice ask. I cannot comment on this, because I do not know these tools. There are ways to achieve a considerable amount of customization of SSH connections with existing tools. If you need additional features, you should sell your change with a more specific justification, including examples that show reviewers who do not know the tools you are using what is needed, but missing. -- Hannes ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 16:10 ` Junio C Hamano 2026-03-27 16:49 ` Wesley @ 2026-03-27 21:51 ` brian m. carlson 2026-03-27 22:25 ` Junio C Hamano 1 sibling, 1 reply; 22+ messages in thread From: brian m. carlson @ 2026-03-27 21:51 UTC (permalink / raw) To: Junio C Hamano; +Cc: Johannes Sixt, Wesley Schwengle, git [-- Attachment #1: Type: text/plain, Size: 635 bytes --] On 2026-03-27 at 16:10:33, Junio C Hamano wrote: > I somehow thought that this practice is so widespread that it was > one of the few first things any new people learn to do, but perhaps > we do not have a good documentation coverage? I actually added this to the Git FAQ: https://git-scm.com/docs/gitfaq#multiple-accounts-ssh. It was added because I saw the question a lot online but we never documented how to do this. Certainly we might want to improve the documentation (patches welcome), but I would not honestly say we have bad documentation coverage here. -- brian m. carlson (they/them) Toronto, Ontario, CA [-- Attachment #2: signature.asc --] [-- Type: application/pgp-signature, Size: 325 bytes --] ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH 0/3] Add support for per-remote and per-namespace SSH options 2026-03-27 21:51 ` brian m. carlson @ 2026-03-27 22:25 ` Junio C Hamano 0 siblings, 0 replies; 22+ messages in thread From: Junio C Hamano @ 2026-03-27 22:25 UTC (permalink / raw) To: brian m. carlson; +Cc: Johannes Sixt, Wesley Schwengle, git "brian m. carlson" <sandals@crustytoothpaste.net> writes: > On 2026-03-27 at 16:10:33, Junio C Hamano wrote: >> I somehow thought that this practice is so widespread that it was >> one of the few first things any new people learn to do, but perhaps >> we do not have a good documentation coverage? > > I actually added this to the Git FAQ: > https://git-scm.com/docs/gitfaq#multiple-accounts-ssh. It was added > because I saw the question a lot online but we never documented how to > do this. > > Certainly we might want to improve the documentation (patches welcome), > but I would not honestly say we have bad documentation coverage here. OK, so that is not lack of documentation but insufficient searching ;-). ^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-03-28 7:47 UTC | newest] Thread overview: 22+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-03-26 23:37 [PATCH 0/3] Add support for per-remote and per-namespace SSH options Wesley Schwengle 2026-03-26 23:37 ` [PATCH 1/3] connect: Rename name to command in connect_git() Wesley Schwengle 2026-03-27 21:33 ` Jeff King 2026-03-28 0:58 ` Wesley 2026-03-28 1:44 ` Jeff King 2026-03-28 2:01 ` Wesley 2026-03-26 23:37 ` [PATCH 2/3] connect: Add transport->remote->name to git_connect() Wesley Schwengle 2026-03-27 21:39 ` Jeff King 2026-03-26 23:37 ` [PATCH 3/3] connect: Add support for per-remote and per-namespace SSH options Wesley Schwengle 2026-03-27 21:45 ` Jeff King 2026-03-28 0:43 ` Wesley 2026-03-28 2:03 ` Jeff King 2026-03-28 2:25 ` Wesley 2026-03-27 7:51 ` [PATCH 0/3] " Johannes Sixt 2026-03-27 15:04 ` Wesley 2026-03-27 16:10 ` Junio C Hamano 2026-03-27 16:49 ` Wesley 2026-03-27 22:06 ` brian m. carlson 2026-03-28 1:02 ` Wesley 2026-03-28 7:46 ` Johannes Sixt 2026-03-27 21:51 ` brian m. carlson 2026-03-27 22:25 ` Junio C Hamano
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox