Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Merge remote-tracking branch 'g1t/main' into ship-42-43

syntaqxcommitted Parentsead4bf6821cb2aBrowse files
10 files+77−290/10 viewed
+2−2
101101 fills in when it is ready. A file's blame at a commit is kept once worked
102102 out, so opening it again, or from a commit's link, shows at once. Search
103103 engines and other crawlers are asked to leave blame pages alone
104−(`Disallow: /*?blame=` in `/robots.txt`); the file itself stays open to
105−them.
104+(`Disallow: /*?blame=`, and `/*/*/blame/` for the blame data the view
105+fetches, in `/robots.txt`); the file itself stays open to them.
106106
107107 ### Why over the API
108108
+1−1
153153
154154 ## Changes to this policy
155155
156−When we change this policy, we'll update the date on the [policies](/policies) page and list the change in its history. **We'll tell you before material changes take effect**, by email to account holders, at least 30 days ahead.
156+When we change this policy, we'll update the date on the [policies](/policies) page and list the change in its history. Changes take effect when they're posted there, so **please check it from time to time**.
157157
158158 ## Contact
159159
+1−1
141141
142142 ## 17. Changes to these terms
143143
144−We'll update these terms as g1t changes. **We'll tell you before material changes take effect**: by email to workspace owners and on the [policies](/policies) page, at least 30 days ahead, unless a change is needed sooner for legal or security reasons. Every change is listed in the policies' change history. To the extent permitted by law, continuing to use g1t after a change takes effect means you accept the updated terms. Where express consent is required, we will ask for it; continued use alone does not replace that consent. If you don't agree, you can stop using g1t and take your data with you.
144+We'll update these terms as g1t changes. Changes take effect when they're posted on the [policies](/policies) page, so **please check it from time to time**. Every change is listed in its change history. To the extent permitted by law, continuing to use g1t after a change takes effect means you accept the updated terms. Where express consent is required, we will ask for it; continued use alone does not replace that consent. If you don't agree, you can stop using g1t and take your data with you.
145145
146146 ## 18. The rest
147147
+1−1
102102 {
103103 date: "2026-10-14",
104104 change:
105− "Removed the draft banner. Standardized contact addresses on support@g1t.sh and named Delaware in the governing-law clause. Clarified automatic renewal, consumer cancellation and refund rights, and consent for changes where required. Privacy Policy: clarified analytics consent and the limits of credential redaction; removed unverified assurances about provider contracts and transfer arrangements. Privacy Policy and Subprocessors: Cloudflare Turnstile protects email sign-up. Privacy Policy: distinguished ordinary closure from personal-data erasure requests, shared content review, and minimal deletion records.",
105+ "Removed the draft banner. Standardized contact addresses on support@g1t.sh and named Delaware in the governing-law clause. Clarified automatic renewal, consumer cancellation and refund rights, and consent for changes where required. Privacy Policy: clarified analytics consent and the limits of credential redaction; removed unverified assurances about provider contracts and transfer arrangements. Privacy Policy and Subprocessors: Cloudflare Turnstile protects email sign-up. Privacy Policy: distinguished ordinary closure from personal-data erasure requests, shared content review, and minimal deletion records. Terms, Privacy Policy and this page: changes take effect when posted on this page, without emailed notice or a fixed 30-day notice period.",
106106 },
107107 {
108108 date: "2026-10-13",
+1−0
88 assert.match(text, /^User-agent: \*\nAllow: \/\n/);
99 assert.match(text, /^Disallow: \/\*\?blame=$/m);
1010 assert.match(text, /^Disallow: \/\*&blame=$/m);
11+ assert.match(text, /^Disallow: \/\*\/\*\/blame\/$/m);
1112 assert.match(text, /^Sitemap: https:\/\/g1t\.example\/sitemap\.xml$/m);
1213 assert.doesNotMatch(text, /docs\.g1t\.sh/, "a self-hosted g1t has no docs site");
1314 assert.ok(text.endsWith("\n"));
+3−2
11 /**
22 * What /robots.txt says: everything may be crawled but a file's blame
3− * (`?blame=1`), and where the site's maps are.
3+ * (`?blame=1`, and the `/<owner>/<repo>/blame/…` data it fetches), and
4+ * where the site's maps are.
45 *
56 * Blame is the costliest file view to work out, one per file per commit a
67 * crawler finds, and adds nothing an index needs that the file itself does
1314 const DOCS_SITEMAP = "https://docs.g1t.sh/sitemap-index.xml";
1415
1516 /** Paths crawlers are kept from, wherever the query puts `blame`. */
16−export const DISALLOWED = ["/*?blame=", "/*&blame="];
17+export const DISALLOWED = ["/*?blame=", "/*&blame=", "/*/*/blame/"];
1718
1819 export function robotsTxt(site: string): string {
1920 const lines = ["User-agent: *", "Allow: /", ...DISALLOWED.map((path) => `Disallow: ${path}`), "", `Sitemap: ${site}/sitemap.xml`];
+2−0
248248 route(":owner/:repo/production-elsewhere.jpg", "routes/repo/production-elsewhere.ts"),
249249 // A file as it is, sent on to the usercontent origin (lib/usercontent.ts).
250250 route(":owner/:repo/raw/:ref/*", "routes/repo/raw.ts"),
251+ // A file's blame as JSON, fetched by its blame view (routes/repo/blob.tsx).
252+ route(":owner/:repo/blame/:ref/*", "routes/repo/blame-data.ts"),
251253 // A project: its overview first, its repository's code under Code. The
252254 // 1:1 project of a repository has the repository's name, so every
253255 // repository address below keeps working.
+2−2
6161 <section className="mt-14 max-w-3xl">
6262 <h2 className="text-xl font-semibold tracking-tight">Changes</h2>
6363 <p className="mt-2 text-sm text-muted">
64− We'll tell you before material changes take effect: by email to account holders and here, at least 30 days
65− ahead, unless the law or a security problem needs a change sooner. Every change is listed below.
64+ g1t is young and these policies change as it does. Changes take effect when they're posted here, so check back
65+ from time to time. Every change is listed below.
6666 </p>
6767 <Card asChild tone="plain" divided className="mt-5">
6868 <ol>
+32−0
1+/**
2+ * A file's blame at a ref, as JSON, for the blame view to fetch once the
3+ * file is on screen: `/<owner>/<repo>/blame/<ref>/<path>`. A first blame
4+ * walks the file's history in repos and can take longer than the page's
5+ * stream may stay open (entry.server.tsx `streamTimeout`), so it is asked
6+ * for on its own request instead; once worked out, repos keeps it and it
7+ * comes back at once. Kept out of indexes like the blame view
8+ * (lib/robots.ts).
9+ */
10+import type { Route } from "./+types/blame-data";
11+import { showCommits } from "../../lib/commit-people.server";
12+import { repos } from "../../lib/services.server";
13+import { getViewer } from "../../lib/session.server";
14+
15+const HEADERS = {
16+ "cache-control": "private, no-store",
17+ "x-robots-tag": "noindex, nofollow",
18+};
19+
20+export async function loader({ params, context }: Route.LoaderArgs) {
21+ const viewer = getViewer(context);
22+ const path = { namespace: params.owner, name: params.repo };
23+ const file = params["*"] ?? "";
24+ const found = await repos.blame(path, viewer, params.ref, file).catch(() => null);
25+ if (!found?.ok) {
26+ const status = found?.error.code === "not_found" ? 404 : 502;
27+ return Response.json({ blame: null }, { status, headers: HEADERS });
28+ }
29+ // Every commit's people at once.
30+ const blame = { ...found.value, commits: await showCommits(found.value.commits) };
31+ return Response.json({ blame }, { headers: HEADERS });
32+}
+32−20
11 import { isCodeownersPath } from "@g1t/contracts";
2+import { useEffect, useState } from "react";
23
34 import type { Route } from "./+types/blob";
45 import { page } from "../../lib/meta";
56 import { CodeownersFileErrors } from "../../components/codeowners";
67 import { BlobView } from "../../components/repo-view";
78 import type { ShownBlame } from "../../lib/commit-people";
8−import { useArrived } from "../../lib/use-arrived";
99 import { highlightLines } from "../../lib/highlight.server";
1010 import { repos, work } from "../../lib/services.server";
11−import { showCommits } from "../../lib/commit-people.server";
1211 import { redirectIfBranchRenamed } from "../../lib/branch-redirect.server";
1312 import { getViewer, unwrap } from "../../lib/session.server";
1413
2423 const path = { namespace: params.owner, name: params.repo };
2524 const file = params["*"] ?? "";
2625 const wantsBlame = new URL(request.url).searchParams.has("blame");
27− // A first blame can take seconds (repos walks the file's history), so it
28− // streams in after the file; a later one comes from repos' cache at once.
29− const blameFound = wantsBlame ? repos.blame(path, viewer, params.ref, file) : null;
30− blameFound?.catch(() => null);
3126 const [found, list, checked] = await Promise.all([
3227 repos.blob(path, viewer, params.ref, file),
3328 // For the branch menu; the page still shows without it.
4136 if (!found.ok && found.error.code === "not_found") await redirectIfBranchRenamed(request, path, viewer, params.ref);
4237 const blob = unwrap(found);
4338 const html = blob.text == null ? null : await highlightLines(blob.path, blob.text);
44− // Each line's commit, with every commit's people at once; null when it
45− // could not be worked out, and the file shows as it is.
46− const blame: Promise<ShownBlame | null> | null =
47− blameFound && blob.text != null
48− ? blameFound
49− .then(async (answered) => (answered.ok ? { ...answered.value, commits: await showCommits(answered.value.commits) } : null))
50− .catch(() => null)
51− : null;
52− return { blob, html, blame, branches, codeowners };
39+ // The blame itself is fetched by the page once the file shows
40+ // (routes/repo/blame-data.ts): a first one can take longer than the
41+ // page's stream may stay open.
42+ return { blob, html, wantsBlame: wantsBlame && blob.text != null, branches, codeowners };
5343 }
5444
5545 export default function Blob({ loaderData, params }: Route.ComponentProps) {
56− const { codeowners, blob, html } = loaderData;
46+ const { codeowners, blob, html, wantsBlame } = loaderData;
5747 // Its errors' lines are marked, when they are this file's.
5848 const own = codeowners != null && codeowners.path === blob.path;
59− // undefined while the blame is on its way, null when it could not be had.
60− const blame = useArrived(loaderData.blame ?? NO_BLAME, undefined);
49+ const blame = useBlame(wantsBlame ? `/${params.owner}/${params.repo}/blame/${encodeURIComponent(blob.ref)}/${encodePath(blob.path)}` : null);
6150 return (
6251 <BlobView
6352 blob={blob}
6453 html={html}
65− blame={loaderData.blame == null ? null : blame === undefined ? "waiting" : blame === null ? "failed" : { blame, lines: html }}
54+ blame={blame === null ? null : typeof blame === "string" ? blame : { blame, lines: html }}
6655 branches={loaderData.branches}
6756 notice={
6857 codeowners && (
7463 );
7564 }
7665
77−const NO_BLAME: Promise<null> = Promise.resolve(null);
66+/** The blame at `url`: "waiting" until it comes, "failed" when it could not
67+ * be had, null when none was asked for. */
68+function useBlame(url: string | null): ShownBlame | "waiting" | "failed" | null {
69+ const [answered, setAnswered] = useState<{ url: string; blame: ShownBlame | "failed" } | null>(null);
70+ useEffect(() => {
71+ if (!url) return;
72+ const stop = new AbortController();
73+ fetch(url, { signal: stop.signal, headers: { accept: "application/json" } })
74+ .then(async (response) => {
75+ const body = (await response.json()) as { blame: ShownBlame | null };
76+ setAnswered({ url, blame: response.ok && body.blame ? body.blame : "failed" });
77+ })
78+ .catch(() => {
79+ if (!stop.signal.aborted) setAnswered({ url, blame: "failed" });
80+ });
81+ return () => stop.abort();
82+ }, [url]);
83+ if (!url) return null;
84+ return answered?.url === url ? answered.blame : "waiting";
85+}
86+
87+function encodePath(path: string): string {
88+ return path.split("/").map(encodeURIComponent).join("/");
89+}

Comments

Sign in to comment.