From 276cead826dbdb7bb28bb28806d9cfb32082ddb1 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 15 Oct 2026 13:31:30 +0700 Subject: [PATCH] feat(git identity): one section per git host; CodeCommit SSH key ID makes a plain ssh:// clone work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS CodeCommit logs an SSH key in by its IAM SSH key ID (APKA…), so a plain `git clone ssh://git-codecommit..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://@/".insteadOf "ssh:///"` 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 --- .../settings/GitIdentitySettings.tsx | 422 +++++++++++++++--- .../__tests__/GitIdentitySettings.test.tsx | 82 +++- client/src/i18n/locales/en/settings.json | 33 +- client/src/i18n/locales/vi/settings.json | 31 +- client/src/lib/api.ts | 7 + docs/API.md | 2 + docs/SERVER-MODE.md | 2 +- server/__tests__/git-clone.test.js | 50 +++ server/lib/git-identity.js | 79 +++- server/routes/git-identity.js | 31 +- 10 files changed, 630 insertions(+), 109 deletions(-) diff --git a/client/src/components/settings/GitIdentitySettings.tsx b/client/src/components/settings/GitIdentitySettings.tsx index bd605afb..90381c48 100644 --- a/client/src/components/settings/GitIdentitySettings.tsx +++ b/client/src/components/settings/GitIdentitySettings.tsx @@ -7,7 +7,9 @@ * keys a clone (Add workspace → Clone) and the pushes from that clone use — * an ED25519 and/or an RSA one (AWS CodeCommit takes only RSA) — make one, copy its public half to add to GitHub/GitLab, replace or delete * it, and whether git typed in the account's own terminal uses it too (a - * switch over `core.sshCommand` in its `~/.gitconfig`, issue #82) — and, for + * switch over `core.sshCommand` in its `~/.gitconfig`, issue #82), the SSH + * login name per host (AWS CodeCommit's SSH key ID, a `url.<…>.insteadOf` in + * that same file, so a plain `ssh://git-codecommit…` URL works) — and, for * a user with their own Linux account (S3), their own HTTPS * logins. The host account's HTTPS logins are the shared Git credentials * table above it, so this lists them only for a member. The private key and @@ -15,7 +17,7 @@ * seen by a non-admin is read-only. * @author Nguyễn Ngọc Trí Vĩ */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Copy, KeyRound, Plus, RefreshCw, Trash2 } from "lucide-react"; import { api, type GitIdentity } from "../../lib/api"; @@ -30,6 +32,140 @@ const INPUT = const BTN = "inline-flex items-center gap-1.5 rounded border border-border-light px-2.5 py-1 text-xs text-fg-secondary hover:bg-surface-3 disabled:cursor-not-allowed disabled:opacity-40"; +const CODECOMMIT_RE = /^git-codecommit\.([a-z0-9-]+)\.amazonaws\.com$/; +// The regions AWS CodeCommit runs in. +const AWS_REGIONS = [ + "ap-southeast-1", + "ap-southeast-2", + "ap-southeast-3", + "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-south-1", + "ap-east-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-central-1", + "eu-north-1", + "eu-south-1", + "sa-east-1", + "me-south-1", + "af-south-1", + "il-central-1", +]; + +function Step({ n, children }: { n: number; children: ReactNode }) { + return ( +
+ + {n} + +
{children}
+
+ ); +} + +function Code({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +type Key = NonNullable[number]; + +/** + * One kind of git host and the key it takes: no key → a Generate button; + * a key → step 1 (copy its public half) followed by the host's own steps. + */ +function KeyCard({ + title, + sub, + type, + k, + id, + busy, + onGenerate, + onRemove, + children, +}: { + title: string; + sub: string; + type: "ed25519" | "rsa"; + k: Key | undefined; + id: GitIdentity; + busy: boolean; + onGenerate: (type: "ed25519" | "rsa") => Promise; + onRemove: (type: "ed25519" | "rsa") => Promise; + children: ReactNode; +}) { + const { t } = useTranslation("settings"); + return ( +
+
+
+
{title}
+
{sub}
+
+ {id.writable && ( + + )} + {k && id.writable && ( + + )} +
+ {k ? ( + <> + +
+ {t("gitIdentity.stepCopy")} + +
+