Discovery matched a git remote's host against the instance URL's host, so a server answering SSH on a different name than its web UI resolved to nothing — and said so only in a debug log. Resolution now runs in three stages. A remote host is matched directly, then against configured aliases, and failing both the instances are asked for the repository: Gitea publishes its SSH hostname in a repository's ssh_url, so the right instance identifies itself. What that turns up is saved as a host alias, so later repositories on the same host resolve with no lookup at all, and the mapping is visible and editable rather than hidden. Each unknown host is probed at most once per session. When nothing resolves the sidebar now names the unmatched host and offers Add Host Alias, instead of showing an empty section. Aliases can also be written by hand as "remote-host = instance URL", accepting =, -> and =>, ignoring ports, and skipping # comments. List preferences now merge workspace entries onto global ones rather than letting an empty global array mask them. Adds Tests/host-aliases.test.js covering both directions: unmatched hosts reported and nothing persisted, a hand-written alias, detection from ssh_url, and a later repository resolving from the stored alias without a probe. 131 checks across three suites. Also adds CLAUDE.md, and .gitea/workflows/ci.yml running the suites, script syntax checks, manifest validation, and a generated-image check. Tools/make-icons.py gains --check, which compares decompressed pixels so a differing zlib version cannot fail it spuriously. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MQuusXgZC2dzwpJJ1qhtti
199 lines
8.6 KiB
JavaScript
199 lines
8.6 KiB
JavaScript
//
|
|
// Instances that answer SSH on a different hostname than their web UI.
|
|
//
|
|
// The remote host is what discovery matches against, so `[email protected]:…`
|
|
// on an instance at `https://gitea.test` resolves to nothing until an alias
|
|
// exists — either configured by hand, or detected once from the ssh_url the
|
|
// repository advertises and then reused from preferences ever after.
|
|
//
|
|
|
|
const { EXTENSION_ROOT } = require("./nova-stub.js");
|
|
require("./fake-gitea.js");
|
|
const { Checker } = require("./assert.js");
|
|
|
|
const path = require("node:path");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const { execFileSync } = require("node:child_process");
|
|
|
|
const script = (...parts) => path.join(EXTENSION_ROOT, "Scripts", ...parts);
|
|
const t = new Checker();
|
|
|
|
const INSTANCE = "https://gitea.test";
|
|
const SSH_HOST = "ssh.gitea.test";
|
|
|
|
const { InstanceRouter } = require(script("gitea", "router.js"));
|
|
const credentials = require(script("gitea", "credentials.js"));
|
|
const discovery = require(script("gitea", "discovery.js"));
|
|
const config = require(script("util", "config.js"));
|
|
|
|
// -- fixtures --------------------------------------------------------------
|
|
|
|
const temporary = [];
|
|
|
|
/** A throwaway workspace holding one git repository with the given remote. */
|
|
function workspaceWith(name, remoteUrl) {
|
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "gitea-nova-alias-"));
|
|
temporary.push(workspace);
|
|
|
|
const repo = path.join(workspace, name);
|
|
fs.mkdirSync(repo, { recursive: true });
|
|
fs.writeFileSync(path.join(repo, "README.md"), `# ${name}\n`);
|
|
|
|
const git = (...args) => execFileSync("git", args, { cwd: repo, encoding: "utf-8" });
|
|
git("init", "-q", "-b", "main");
|
|
git("config", "user.email", "[email protected]");
|
|
git("config", "user.name", "Test");
|
|
git("remote", "add", "origin", remoteUrl);
|
|
git("add", "-A");
|
|
git("commit", "-qm", "initial");
|
|
|
|
return workspace;
|
|
}
|
|
|
|
function cleanup() {
|
|
for (const dir of temporary) fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
/** A fresh router with the alias preference set to `entries`. */
|
|
function routerWith(entries) {
|
|
nova.config.set("gitea.hostAliases", entries);
|
|
return new InstanceRouter();
|
|
}
|
|
|
|
nova.workspace.path = workspaceWith("widget", `git@${SSH_HOST}:acme/widget.git`);
|
|
nova.config.set("gitea.baseUrl", INSTANCE);
|
|
|
|
(async () => {
|
|
t.section("alias parsing");
|
|
for (const [label, entry] of [
|
|
["equals separator", `${SSH_HOST} = ${INSTANCE}`],
|
|
["arrow separator", `${SSH_HOST} -> ${INSTANCE}`],
|
|
["fat arrow separator", `${SSH_HOST}=>${INSTANCE}`],
|
|
["bare host as the target", `${SSH_HOST} = gitea.test`],
|
|
["untidy whitespace", ` ${SSH_HOST} = ${INSTANCE} `],
|
|
["ports ignored on both sides", `${SSH_HOST}:2222 = ${INSTANCE}`],
|
|
["case-insensitive", `${SSH_HOST.toUpperCase()} = ${INSTANCE}`],
|
|
]) {
|
|
t.equals(label, routerWith([entry]).baseUrlForHost(SSH_HOST), INSTANCE);
|
|
}
|
|
t.equals(
|
|
"a direct host match still wins",
|
|
routerWith([`${SSH_HOST} = ${INSTANCE}`]).baseUrlForHost("gitea.test"),
|
|
INSTANCE,
|
|
);
|
|
|
|
t.section("alias entries that should be ignored");
|
|
for (const [label, entry] of [
|
|
["blank", " "],
|
|
["comment", `# ${SSH_HOST} = ${INSTANCE}`],
|
|
["no separator", `${SSH_HOST} ${INSTANCE}`],
|
|
["too many separators", `${SSH_HOST} = ${INSTANCE} = extra`],
|
|
["self-referential", `${SSH_HOST} = ${SSH_HOST}`],
|
|
]) {
|
|
t.equals(label, routerWith([entry]).baseUrlForHost(SSH_HOST), null);
|
|
}
|
|
t.equals(
|
|
"an unrelated host stays unmatched",
|
|
routerWith([`${SSH_HOST} = ${INSTANCE}`]).baseUrlForHost("github.com"),
|
|
null,
|
|
);
|
|
|
|
t.section("unmatched: no alias and nothing to ask");
|
|
// Without a token there is no instance to interrogate, so this is the pure
|
|
// host-matching path and it must fail visibly rather than silently.
|
|
nova.config.set("gitea.hostAliases", []);
|
|
credentials.removeToken(INSTANCE);
|
|
|
|
const bare = new InstanceRouter();
|
|
const unmatched = await discovery.discover(bare);
|
|
t.equals("no repository resolves", unmatched.repos.length, 0);
|
|
t.equals("the unmatched host is reported", unmatched.unmatched.map((u) => u.host), [SSH_HOST]);
|
|
t.equals(
|
|
"the report carries the remote for context",
|
|
unmatched.unmatched[0]?.remoteUrl,
|
|
`git@${SSH_HOST}:acme/widget.git`,
|
|
);
|
|
t.equals("and nothing was persisted", nova.config.get("gitea.hostAliases", "array"), []);
|
|
|
|
t.section("matched: alias configured by hand");
|
|
const configured = routerWith([`${SSH_HOST} = ${INSTANCE}`]);
|
|
const viaConfig = await discovery.discover(configured);
|
|
t.equals("the repository resolves", viaConfig.repos.length, 1);
|
|
t.equals("routed to the web instance", viaConfig.repos[0]?.baseUrl, INSTANCE);
|
|
t.equals("owner and name come from the remote", viaConfig.repos[0]?.fullName, "acme/widget");
|
|
t.equals("nothing left unmatched", viaConfig.unmatched.length, 0);
|
|
|
|
t.section("matched: alias detected from the API");
|
|
// No alias configured, but a token is available. The instance is asked for
|
|
// the repository and identifies itself through the ssh_url it advertises.
|
|
nova.config.set("gitea.hostAliases", []);
|
|
credentials.setToken(INSTANCE, "tok_test");
|
|
|
|
const detecting = new InstanceRouter();
|
|
const detected = await discovery.discover(detecting);
|
|
t.equals("the repository resolves with no configuration", detected.repos.length, 1);
|
|
t.equals("routed to the web instance", detected.repos[0]?.baseUrl, INSTANCE);
|
|
t.equals("nothing left unmatched", detected.unmatched.length, 0);
|
|
t.check("the host was probed", detecting._probedHosts.has(SSH_HOST));
|
|
t.check(
|
|
"the alias is live on the router",
|
|
detecting.aliases.get(SSH_HOST) === "gitea.test",
|
|
detecting.aliases.get(SSH_HOST),
|
|
);
|
|
t.check(
|
|
"and was written to preferences",
|
|
(nova.config.get("gitea.hostAliases", "array") || []).includes(`${SSH_HOST} = ${INSTANCE}`),
|
|
JSON.stringify(nova.config.get("gitea.hostAliases", "array")),
|
|
);
|
|
|
|
t.section("a later repository on the same host");
|
|
// What the stored alias buys: a different repository, in a different
|
|
// workspace, resolves straight away without another round trip.
|
|
nova.workspace.path = workspaceWith("gadget", `git@${SSH_HOST}:acme/gadget.git`);
|
|
|
|
const returning = new InstanceRouter();
|
|
const later = await discovery.discover(returning);
|
|
t.equals("the new repository resolves", later.repos.length, 1);
|
|
t.equals("identified correctly", later.repos[0]?.fullName, "acme/gadget");
|
|
t.equals("routed to the web instance", later.repos[0]?.baseUrl, INSTANCE);
|
|
t.equals("nothing left unmatched", later.unmatched.length, 0);
|
|
t.equals("and no probe was needed", returning._probedHosts.size, 0);
|
|
|
|
t.section("detection is not repeated");
|
|
// A host belonging to no instance must not be probed on every refresh.
|
|
nova.config.set("gitea.hostAliases", []);
|
|
const probing = new InstanceRouter();
|
|
t.equals("first probe finds nothing", await probing.detectHostAlias("elsewhere.test", "a", "b"), null);
|
|
t.check("the host is remembered as probed", probing._probedHosts.has("elsewhere.test"));
|
|
t.equals("a second probe short-circuits", await probing.detectHostAlias("elsewhere.test", "a", "b"), null);
|
|
probing.invalidate();
|
|
t.check("an explicit refresh clears the record", !probing._probedHosts.has("elsewhere.test"));
|
|
|
|
t.section("persisting an alias");
|
|
nova.config.set("gitea.hostAliases", []);
|
|
t.equals("a new alias is written", config.addHostAlias(SSH_HOST, INSTANCE), true);
|
|
t.equals("writing the same alias again is a no-op", config.addHostAlias(SSH_HOST, INSTANCE), false);
|
|
t.equals(
|
|
"re-pointing a host replaces rather than duplicates",
|
|
config.addHostAlias(SSH_HOST, "https://other.test"),
|
|
true,
|
|
);
|
|
t.equals("one entry per host", (nova.config.get("gitea.hostAliases", "array") || []).length, 1);
|
|
|
|
t.section("workspace aliases add to global ones");
|
|
nova.config.set("gitea.hostAliases", [`${SSH_HOST} = ${INSTANCE}`]);
|
|
nova.workspace.config.set("gitea.hostAliases", [`other.test = ${INSTANCE}`]);
|
|
const merged = new InstanceRouter();
|
|
t.equals("global entry applies", merged.baseUrlForHost(SSH_HOST), INSTANCE);
|
|
t.equals("workspace entry applies", merged.baseUrlForHost("other.test"), INSTANCE);
|
|
nova.workspace.config.remove("gitea.hostAliases");
|
|
|
|
cleanup();
|
|
t.finish("HOST ALIAS CHECKS");
|
|
})().catch((error) => {
|
|
console.error("HARNESS ERROR", error);
|
|
cleanup();
|
|
process.exit(2);
|
|
});
|