feat(git identity): one section per git host; CodeCommit SSH key ID makes a plain ssh:// clone work
AWS CodeCommit logs an SSH key in by its IAM SSH key ID (APKA…), so a plain `git clone ssh://git-codecommit.<region>.amazonaws.com/v1/repos/x` was refused as the Linux account name. The account can now save an SSH user per host — PUT/DELETE /api/me/git/ssh-user, listed as `sshUsers` — written as `url."ssh://<user>@<host>/".insteadOf "ssh://<host>/"` in its own ~/.gitconfig (as the member under S3), so the plain URL works in a pane, in Clone and in the git bar. Only rules of exactly that shape are listed or removed; any other insteadOf is the user's and left alone. Settings → Git → Your git identity now shows the SSH keys one section per kind of host, each with only its own numbered steps: GitHub · GitLab · Gitea · Bitbucket (ED25519) and AWS CodeCommit (RSA: copy → upload in IAM → paste the key ID and pick the region, then a "Ready" clone line), plus a collapsed "Other SSH hosts (advanced)". The shared key-type picker is gone: each section has its own Generate/Replace/Delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
* A plain `git` typed in a terminal reads none of that, so the account's own
|
||||
* `~/.gitconfig` may also carry our `core.sshCommand` (`setTerminalSsh`: on
|
||||
* by default for a member, a switch for the host; a user's own is never
|
||||
* overwritten).
|
||||
* overwritten), plus a per-host SSH login name (`setSshUser`: AWS CodeCommit
|
||||
* wants the IAM SSH key ID as the user) as a `url.<…>.insteadOf` rule there.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -468,6 +469,79 @@ async function setTerminalSsh(account, on) {
|
||||
return terminalSshState(account);
|
||||
}
|
||||
|
||||
// ── SSH login name per host (`~/.gitconfig` url.<…>.insteadOf) ─────────────
|
||||
|
||||
// AWS CodeCommit logs an SSH key in by its IAM key ID (`APKA…`) as the user
|
||||
// name, so `ssh://git-codecommit…/v1/repos/x` (no user) is refused. One
|
||||
// `url."ssh://<user>@<host>/".insteadOf "ssh://<host>/"` per host makes the
|
||||
// plain URL work for every git of the account — a clone typed in a pane, the
|
||||
// Clone dialog's and the git bar's — without touching `~/.ssh/config`. The
|
||||
// user name is no secret (an ID, not a key), so it may cross sudo's argv.
|
||||
const SSH_USER_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$/;
|
||||
const insteadOfBase = (host, user) => `ssh://${user}@${host}/`;
|
||||
|
||||
/** `[{host, user}]` — only the rules of exactly our shape; any other insteadOf is the user's. */
|
||||
async function listSshUsers(account) {
|
||||
const { git } = require("./worktree");
|
||||
const out = await git(homeOf(account), [
|
||||
"config",
|
||||
"--global",
|
||||
"--get-regexp",
|
||||
"^url\\..*\\.insteadof$",
|
||||
]).then(
|
||||
(r) => r.stdout,
|
||||
() => ""
|
||||
);
|
||||
const rows = [];
|
||||
for (const line of out.split("\n")) {
|
||||
const m = /^url\.ssh:\/\/([^@/\s]+)@([^/\s]+)\/\.insteadof ssh:\/\/([^/\s]+)\/$/.exec(line);
|
||||
if (m && m[2] === m[3] && SSH_USER_RE.test(m[1]) && HOST_RE.test(m[2])) {
|
||||
rows.push({ host: m[2], user: m[1] });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Drop our rule for `host` (whatever its user); false when there was none. */
|
||||
async function removeSshUser(account, host) {
|
||||
const { git } = require("./worktree");
|
||||
const h = String(host || "").toLowerCase();
|
||||
const rows = (await listSshUsers(account)).filter((r) => r.host === h);
|
||||
for (const r of rows) {
|
||||
await git(homeOf(account), [
|
||||
"config",
|
||||
"--global",
|
||||
"--fixed-value",
|
||||
"--unset-all",
|
||||
`url.${insteadOfBase(r.host, r.user)}.insteadOf`,
|
||||
`ssh://${r.host}/`,
|
||||
]);
|
||||
}
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/** Log in to `host` over SSH as `user` (replaces our earlier rule for that host). */
|
||||
async function setSshUser(account, { host, user } = {}) {
|
||||
const h = String(host || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const u = String(user || "").trim();
|
||||
if (!HOST_RE.test(h)) {
|
||||
throw err("EBADHOST", "a host name like git-codecommit.us-east-1.amazonaws.com");
|
||||
}
|
||||
if (!SSH_USER_RE.test(u)) throw err("EBADSSHUSER", "a user name of letters, digits, . _ -");
|
||||
await removeSshUser(account, h);
|
||||
const { git } = require("./worktree");
|
||||
await git(homeOf(account), [
|
||||
"config",
|
||||
"--global",
|
||||
"--add",
|
||||
`url.${insteadOfBase(h, u)}.insteadOf`,
|
||||
`ssh://${h}/`,
|
||||
]);
|
||||
return listSshUsers(account);
|
||||
}
|
||||
|
||||
// ── Network commands in an existing repo (git bar fetch/pull/push) ─────────
|
||||
|
||||
/**
|
||||
@@ -714,6 +788,9 @@ module.exports = {
|
||||
terminalSshCommand,
|
||||
terminalSshState,
|
||||
setTerminalSsh,
|
||||
listSshUsers,
|
||||
setSshUser,
|
||||
removeSshUser,
|
||||
listHttps,
|
||||
saveHttps,
|
||||
removeHttps,
|
||||
|
||||
Reference in New Issue
Block a user