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
238 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
2 * A project's place in the catalog, put together from what its files say
3 * (see `./extract`) and what g1t knows about it already: its dependencies,
4 * owners, deployments and integrations. Pure, so a rebuild from the same
5 * inputs gives the same entities and relations, and writing them is
6 * idempotent.
7 */
8
9import type { EntityKind, RelationKind } from "@g1t/contracts";
10
11import type { FileFacts, Hint } from "./extract";
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared12import { harvestable } from "./harvest.ts";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas13
14export type ProjectInput = {
15 id: string;
16 workspace: string;
17 slug: string;
18 name: string;
19 description: string | null;
20 private: boolean;
21 repoId: string;
22 repo: { namespace: string; name: string };
23 rootDir: string;
24 defaultBranch: string;
25};
26
27export type FileRecord = { path: string; facts: FileFacts };
28
29export type Surroundings = {
30 /** Members who own it: named in its files, or who wrote most of it. */
31 owners: string[];
32 deploy: {
33 enabled: boolean;
34 production: { url: string; commit: string; deployedAt: string } | null;
35 previews: number;
36 latest: { status: string; kind: string; error: string | null; createdAt: string } | null;
37 } | null;
38 integrations: { id: string; provider: string; name: string; kind: string; repo: string | null }[];
39};
40
41export type EntityDraft = {
42 kind: EntityKind;
43 key: string;
44 name: string;
45 summary: string | null;
46 data: Record<string, unknown>;
47 ref: string | null;
48};
49
50export type Ref = { kind: EntityKind; key: string };
51export type RelationDraft = { from: Ref; kind: RelationKind; to: Ref };
52
53export type Assembled = {
54 entities: EntityDraft[];
55 relations: RelationDraft[];
56 /** Stack facts and doc hints, with the file each came from. */
57 hints: (Hint & { path: string })[];
58 /** Whether a workflow runs tests. */
59 tests: boolean;
60};
61
62function unique<T>(values: T[]): T[] {
63 return [...new Set(values)];
64}
65
66/** The catalog entries for one project. */
67export function assemble(project: ProjectInput, files: FileRecord[], around: Surroundings): Assembled {
68 const me: Ref = { kind: "project", key: project.slug };
69 const entities: EntityDraft[] = [];
70 const relations: RelationDraft[] = [];
71 const relate = (from: Ref, kind: RelationKind, to: Ref) => relations.push({ from, kind, to });
72
73 const languages = unique(files.flatMap((file) => file.facts.languages));
74 const packages = files.flatMap((file) => file.facts.packages.map((pkg) => ({ ...pkg, path: file.path })));
75 const apis = files.flatMap((file) => file.facts.apis.map((api) => ({ ...api, path: file.path })));
76 const docs = files.flatMap((file) => (file.facts.doc ? [file.facts.doc] : []));
77 const tests = files.some((file) => file.facts.tests);
78 const declaredOwners = unique(files.flatMap((file) => file.facts.owners));
79 const owners = unique([...declaredOwners, ...around.owners]);
80
81 const app: Ref = { kind: "app", key: project.slug };
82 const hasApp = !!around.deploy?.enabled;
83 entities.push({
84 kind: "project",
85 key: project.slug,
86 name: project.name,
87 summary:
88 [
89 project.description,
90 languages.length ? `Written in ${languages.join(", ")}.` : null,
91 packages.length ? `Packages: ${packages.map((pkg) => pkg.name).join(", ")}.` : null,
92 owners.length ? `Owned by ${owners.join(", ")}.` : null,
93 ]
94 .filter(Boolean)
95 .join(" ") || null,
96 data: {
97 repo: `${project.repo.namespace}/${project.repo.name}`,
98 rootDir: project.rootDir,
99 defaultBranch: project.defaultBranch,
100 languages,
101 owners,
102 tests,
103 testCommands: packages.flatMap((pkg) => (pkg.scripts?.test ? [`${pkg.ecosystem === "npm" ? "npm test" : pkg.scripts.test}`] : [])),
104 productionUrl: around.deploy?.production?.url ?? null,
105 },
106 ref: `/${project.repo.namespace}/${project.repo.name}${project.rootDir ? `/tree/${project.defaultBranch}/${project.rootDir}` : ""}`,
107 });
108
109 for (const owner of owners) {
110 entities.push({ kind: "owner", key: owner.toLowerCase(), name: owner, summary: null, data: {}, ref: `/u/${owner}` });
111 relate(me, "owned_by", { kind: "owner", key: owner.toLowerCase() });
112 }
113 for (const language of languages) {
114 entities.push({ kind: "language", key: language.toLowerCase(), name: language, summary: null, data: {}, ref: null });
115 relate(me, "uses", { kind: "language", key: language.toLowerCase() });
116 }
117 for (const pkg of packages) {
118 const key = `${pkg.ecosystem}:${pkg.name}`;
119 entities.push({
120 kind: "package",
121 key,
122 name: pkg.name,
123 summary: `${pkg.ecosystem} package${pkg.version ? ` ${pkg.version}` : ""} in ${project.name}; depends on ${pkg.dependencies.length} packages.`,
124 data: { ecosystem: pkg.ecosystem, version: pkg.version, dependencies: pkg.dependencies, scripts: pkg.scripts ?? {}, members: pkg.members ?? [], path: pkg.path },
125 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, pkg.path].filter(Boolean).join("/")}`,
126 });
127 relate(me, "exposes", { kind: "package", key });
128 // Within the workspace, a package using another is a dependency the
129 // catalog can draw; ones from outside have no entity and are dropped
130 // when read.
131 for (const dependency of pkg.dependencies) {
132 relate({ kind: "package", key }, "depends_on", { kind: "package", key: `${pkg.ecosystem}:${dependency}` });
133 }
134 }
135 for (const api of apis) {
136 const key = `${project.slug}:${api.kind}:${api.name}`;
137 entities.push({
138 kind: "api",
139 key,
140 name: api.name,
141 summary: api.summary,
142 data: { kind: api.kind, routes: api.routes, path: api.path },
143 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, api.path].filter(Boolean).join("/")}`,
144 });
145 relate(me, "exposes", { kind: "api", key });
146 if (hasApp && api.kind === "worker") relate(app, "exposes", { kind: "api", key });
147 }
148 for (const doc of docs) {
149 const key = `${project.slug}:${doc.path}`;
150 entities.push({
151 kind: "doc",
152 key,
153 name: doc.title,
154 summary: doc.summary,
155 data: { path: doc.path, role: doc.role, chunks: doc.chunks.length },
156 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, doc.path].filter(Boolean).join("/")}`,
157 });
158 relate(me, "documented_by", { kind: "doc", key });
159 }
160 if (hasApp && around.deploy) {
161 const { production, previews, latest } = around.deploy;
162 entities.push({
163 kind: "app",
164 key: project.slug,
165 name: project.name,
166 summary: production ? `Live at ${production.url}.` : "Deploys from its default branch; not live yet.",
167 data: { productionUrl: production?.url ?? null, previews, latest },
168 ref: `/${project.repo.namespace}/${project.repo.name}/deployments`,
169 });
170 relate(me, "exposes", app);
171 entities.push({
172 kind: "environment",
173 key: `${project.slug}/production`,
174 name: `${project.name} production`,
175 summary: production ? `${production.url}, from ${production.commit.slice(0, 7)}` : "Not deployed yet",
176 data: { url: production?.url ?? null, commit: production?.commit ?? null, deployedAt: production?.deployedAt ?? null, status: latest?.kind === "production" ? latest.status : production ? "ready" : null },
177 ref: production?.url ?? null,
178 });
179 relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/production` });
180 if (previews > 0) {
181 entities.push({
182 kind: "environment",
183 key: `${project.slug}/preview`,
184 name: `${project.name} previews`,
185 summary: `${previews} preview${previews === 1 ? "" : "s"} up, one per open pull request`,
186 data: { previews },
187 ref: `/${project.repo.namespace}/${project.repo.name}/deployments`,
188 });
189 relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/preview` });
190 }
191 }
192 const repoPath = `${project.repo.namespace}/${project.repo.name}`.toLowerCase();
193 for (const integration of around.integrations) {
194 if (integration.repo?.toLowerCase() === repoPath) relate(me, "uses", { kind: "integration", key: integration.id });
195 }
196
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared197 // A doc is a source of memory only when it says how to work here (see
198 // ./harvest), or project.yml says it is.
199 const memory = files.find((file) => file.facts.memory)?.facts.memory ?? null;
200 const hints = files
201 .filter((file) => !file.facts.doc || harvestable(file.path, file.facts.doc.role, memory))
202 .flatMap((file) => file.facts.hints.map((hint) => ({ ...hint, path: file.path })));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas203 return { entities, relations, hints, tests };
204}
205
206/** A workspace's integrations, as catalog entries of their own. */
207export function integrationEntities(integrations: Surroundings["integrations"]): EntityDraft[] {
208 return integrations.map((integration) => ({
209 kind: "integration",
210 key: integration.id,
211 name: integration.name,
212 summary: `${integration.provider} (${integration.kind})${integration.repo ? `, opens issues on ${integration.repo}` : ""}`,
213 data: { provider: integration.provider, kind: integration.kind, repo: integration.repo },
214 ref: null,
215 }));
216}
217
218/**
219 * The members who wrote most of a project, from its recent commits: those
220 * with at least a fifth of them, at most three. A commit's author is a
221 * member when its name or its email's local part is their username.
222 */
223export function authorsOf(commits: { author: { name: string; email: string } }[], members: string[]): string[] {
224 const byName = new Map(members.map((member) => [member.toLowerCase(), member]));
225 const counts = new Map<string, number>();
226 for (const commit of commits) {
227 const name = commit.author.name.toLowerCase();
228 const local = commit.author.email.split("@")[0]?.toLowerCase() ?? "";
229 const member = byName.get(name) ?? byName.get(local);
230 if (member) counts.set(member, (counts.get(member) ?? 0) + 1);
231 }
232 const total = commits.length || 1;
233 return [...counts.entries()]
234 .filter(([, count]) => count / total >= 0.2)
235 .sort((a, b) => b[1] - a[1])
236 .slice(0, 3)
237 .map(([member]) => member);
238}

This file's history is long; its oldest lines are credited to the oldest commit read.