* [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
* [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
* [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 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 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 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
* 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 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 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 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
* 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 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 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 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
* 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-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
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