Skip to content
90 linesCodeBlameRaw
1import { useRouteLoaderData } from "react-router";
2
3/**
4 * Where this g1t lives: the site, the REST API, the MCP server and the
5 * social-card service. g1t.sh uses the defaults below; a self-hosted g1t
6 * sets its own (app/lib/addresses.server.ts reads them), and the root
7 * loader hands them to every page.
8 */
9export type Addresses = {
10 /** The site's origin, such as `https://g1t.sh`. Git remotes are under it. */
11 site: string;
12 /** The REST API's origin, which is also the OAuth issuer. */
13 api: string;
14 /** The MCP server's URL, as agents are told to add it. */
15 mcp: string;
16 /** The social-card image service's origin, or null when there is none. */
17 og: string | null;
18 /**
19 * Where bytes people supplied are served (repository files, avatars), on
20 * an origin of its own that never sees the site's cookies. Self-hosted
21 * without a host of its own, a path on the site (`<site>/-/usercontent`).
22 */
23 usercontent: string;
24};
25
26export const HOSTED_ADDRESSES: Addresses = {
27 site: "https://g1t.sh",
28 api: "https://api.g1t.sh",
29 mcp: "https://mcp.g1t.sh",
30 og: "https://og.g1t.sh",
31 usercontent: "https://g1tusercontent.com",
32};
33
34/** The Worker settings the addresses come from; every one optional. */
35export type AddressSettings = { SITE_URL?: string; API_URL?: string; MCP_URL?: string; OG_URL?: string; USERCONTENT_URL?: string };
36
37const trim = (value: string) => value.trim().replace(/\/+$/, "");
38
39/**
40 * The addresses from the Worker's settings. An unset or empty setting is
41 * g1t.sh's address, except `OG_URL`: set to an empty string, it means there
42 * is no card service, and pages carry no image tags. `USERCONTENT_URL`
43 * unset is g1tusercontent.com on g1t.sh, and a path on the site wherever
44 * `SITE_URL` names another.
45 */
46export function addressesFor(settings: AddressSettings): Addresses {
47 const pick = (value: string | undefined, fallback: string) => (value ? trim(value) : "") || fallback;
48 const site = pick(settings.SITE_URL, HOSTED_ADDRESSES.site);
49 const ownSite = site === HOSTED_ADDRESSES.site;
50 return {
51 site,
52 usercontent: usercontentOf(pick(settings.USERCONTENT_URL, ownSite ? HOSTED_ADDRESSES.usercontent : `${site}/-/usercontent`), site),
53 api: pick(settings.API_URL, HOSTED_ADDRESSES.api),
54 mcp: pick(settings.MCP_URL, HOSTED_ADDRESSES.mcp),
55 og: settings.OG_URL === undefined ? HOSTED_ADDRESSES.og : trim(settings.OG_URL) || null,
56 };
57}
58
59/**
60 * Where uploaded avatars and repository files are served, from the root
61 * loader's data; the site's own paths when it has none (they redirect).
62 */
63export function usercontentFrom(rootData: unknown): string {
64 return (rootData as { addresses?: Addresses } | null | undefined)?.addresses?.usercontent ?? "";
65}
66
67/** The site's own origin is never the usercontent origin: a path on it is. */
68function usercontentOf(address: string, site: string): string {
69 return address === site ? `${site}/-/usercontent` : address;
70}
71
72/** The addresses in the root loader's data, or g1t.sh's when it has none. */
73export function addressesFrom(rootData: unknown): Addresses {
74 return (rootData as { addresses?: Addresses } | null | undefined)?.addresses ?? HOSTED_ADDRESSES;
75}
76
77/** This g1t's addresses, from the root loader. */
78export function useAddresses(): Addresses {
79 return addressesFrom(useRouteLoaderData("root"));
80}
81
82/** The SSH clone address of a repository (`git@host:owner/name.git`), from its `owner/name`. */
83export function sshUrl(addresses: Pick<Addresses, "site">, path: string): string {
84 return `git@${new URL(addresses.site).hostname}:${path.replace(/^\/+/, "")}.git`;
85}
86
87/** The HTTPS clone address of a repository, from its `owner/name`. */
88export function cloneUrl(addresses: Pick<Addresses, "site">, path: string): string {
89 return `${addresses.site}/${path.replace(/^\/+/, "")}.git`;
90}