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
223 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.

Navigation: the workspace's own page with tabs, pins and recent in the sidebar1/**
2 * A workspace's Projects page, for a workspace with hundreds of them: what
3 * the address asks for (a search, filters, a sort, a view and a page), and
4 * the list that answers it. Read from and written to the address, so a
5 * filtered list can be shared, bookmarked and gone back to.
6 */
7
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.8import type { Project, ProjectEcosystem, ProjectKind } from "@g1t/contracts";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar9
10export type ProjectSort = "updated" | "name" | "active" | "pushed";
11export type ProjectView = "list" | "grid";
12
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.13/** The Kind filter's choices: a kind of project. */
14export type ProjectKindFilter = "all" | ProjectKind;
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.15
Navigation: the workspace's own page with tabs, pins and recent in the sidebar16export type ProjectQuery = {
17 /** Words in the name, slug or description. */
18 q: string;
19 visibility: "all" | "public" | "private";
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.20 kind: ProjectKindFilter;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar21 /** A language, from the manifests at the project's root. */
22 language: string | null;
23 /** Only projects with Deployments on. */
24 deployments: boolean;
25 /** Archived projects: left out (the default), shown too, or only them. */
26 archived: "hide" | "include" | "only";
27 sort: ProjectSort;
28 view: ProjectView;
29 /** 1 first. */
30 page: number;
31};
32
33/** Projects to a page. */
34export const PAGE_SIZE = 30;
35
36export const DEFAULT_QUERY: ProjectQuery = {
37 q: "",
38 visibility: "all",
39 kind: "all",
40 language: null,
41 deployments: false,
42 archived: "hide",
43 sort: "updated",
44 view: "list",
45 page: 1,
46};
47
48export const SORTS: { value: ProjectSort; label: string }[] = [
49 { value: "updated", label: "Recently updated" },
50 { value: "pushed", label: "Recently pushed" },
51 { value: "active", label: "Most active" },
52 { value: "name", label: "Name" },
53];
54
55/** The language a project's manifests say it is written in. */
56export const LANGUAGE_OF: Record<ProjectEcosystem, string> = {
57 npm: "JavaScript",
58 cargo: "Rust",
59 go: "Go",
60 composer: "PHP",
61 python: "Python",
62};
63
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.64/** What the list needs of a project, with whether its Deployments are on. */
Navigation: the workspace's own page with tabs, pins and recent in the sidebar65export type Listed = Pick<
66 Project,
67 "id" | "slug" | "name" | "description" | "private" | "archived" | "kind" | "ecosystem" | "updatedAt" | "pushedAt" | "activity"
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.68> & { deploying: boolean };
Navigation: the workspace's own page with tabs, pins and recent in the sidebar69
70const one = <T extends string>(value: string | null, allowed: readonly T[], fallback: T): T =>
71 (allowed as readonly string[]).includes(value ?? "") ? (value as T) : fallback;
72
73/** What the address asks for; anything it does not say, or says wrong, is the default. */
74export function readProjectQuery(params: URLSearchParams): ProjectQuery {
75 const page = Number.parseInt(params.get("page") ?? "", 10);
76 return {
77 q: (params.get("q") ?? "").trim().slice(0, 100),
78 visibility: one(params.get("visibility"), ["all", "public", "private"] as const, "all"),
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.79 kind: one(params.get("kind"), ["all", "app", "library", "tool", "docs", "other"] as const, "all"),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar80 language: params.get("language")?.trim() || null,
81 deployments: params.get("deployments") === "on",
82 archived: one(params.get("archived"), ["hide", "include", "only"] as const, "hide"),
83 sort: one(params.get("sort"), ["updated", "name", "active", "pushed"] as const, "updated"),
84 view: one(params.get("view"), ["list", "grid"] as const, "list"),
85 page: Number.isFinite(page) && page > 0 ? page : 1,
86 };
87}
88
89/**
90 * The address for `query` with `change` made, leaving out what is the
91 * default. Changing anything but the page goes back to the first page.
92 */
93export function projectQueryString(query: ProjectQuery, change: Partial<ProjectQuery> = {}): string {
94 const next = { ...query, ...change };
95 if (!("page" in change)) next.page = 1;
96 const params = new URLSearchParams();
97 if (next.q) params.set("q", next.q);
98 if (next.visibility !== "all") params.set("visibility", next.visibility);
99 if (next.kind !== "all") params.set("kind", next.kind);
100 if (next.language) params.set("language", next.language);
101 if (next.deployments) params.set("deployments", "on");
102 if (next.archived !== "hide") params.set("archived", next.archived);
103 if (next.sort !== "updated") params.set("sort", next.sort);
104 if (next.view !== "list") params.set("view", next.view);
105 if (next.page > 1) params.set("page", String(next.page));
106 const text = params.toString();
107 return text ? `?${text}` : "";
108}
109
110/** Whether anything narrows the list, beyond leaving archived projects out. */
111export function isFiltered(query: ProjectQuery): boolean {
112 return Boolean(
113 query.q || query.visibility !== "all" || query.kind !== "all" || query.language || query.deployments || query.archived !== "hide",
114 );
115}
116
117/** When it last changed: its own settings, or a push, whichever is later. */
118export function lastUpdated(project: Pick<Listed, "updatedAt" | "pushedAt">): string {
119 return project.pushedAt && project.pushedAt > project.updatedAt ? project.pushedAt : project.updatedAt;
120}
121
122function matches(project: Listed, words: string[]): boolean {
123 const text = `${project.name} ${project.slug} ${project.description ?? ""}`.toLowerCase();
124 return words.every((word) => text.includes(word));
125}
126
127/** The projects `query` asks for, in its order, before paging. */
128export function filterProjects(projects: Listed[], query: ProjectQuery): Listed[] {
129 const words = query.q.toLowerCase().split(/\s+/).filter(Boolean);
130 const shown = projects.filter(
131 (project) =>
132 (query.archived === "include" || (query.archived === "only") === project.archived) &&
133 (query.visibility === "all" || (query.visibility === "private") === project.private) &&
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.134 (query.kind === "all" || project.kind === query.kind) &&
Navigation: the workspace's own page with tabs, pins and recent in the sidebar135 (!query.language || (project.ecosystem != null && LANGUAGE_OF[project.ecosystem] === query.language)) &&
136 (!query.deployments || project.deploying) &&
137 (words.length === 0 || matches(project, words)),
138 );
139 return sortProjects(shown, query.sort, words);
140}
141
142const byName = (a: Listed, b: Listed) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }) || a.slug.localeCompare(b.slug);
143
144/**
145 * In `sort`'s order, ties by name. While searching, a name that starts
146 * with what was typed comes first.
147 */
148export function sortProjects(projects: Listed[], sort: ProjectSort, words: string[] = []): Listed[] {
149 const first = words[0];
150 const leads = (project: Listed) =>
151 first != null && (project.name.toLowerCase().startsWith(first) || project.slug.startsWith(first)) ? 0 : 1;
152 const order: Record<ProjectSort, (a: Listed, b: Listed) => number> = {
153 name: byName,
154 updated: (a, b) => lastUpdated(b).localeCompare(lastUpdated(a)),
155 pushed: (a, b) => (b.pushedAt ?? "").localeCompare(a.pushedAt ?? ""),
156 active: (a, b) => b.activity - a.activity,
157 };
158 return [...projects].sort((a, b) => leads(a) - leads(b) || order[sort](a, b) || byName(a, b));
159}
160
161/** One page of them, and where it sits among the rest. */
162export function pageOf<T>(list: T[], page: number, size = PAGE_SIZE): { items: T[]; page: number; pages: number; from: number; to: number } {
163 const pages = Math.max(1, Math.ceil(list.length / size));
164 const current = Math.min(Math.max(1, page), pages);
165 const start = (current - 1) * size;
166 const items = list.slice(start, start + size);
167 return { items, page: current, pages, from: items.length ? start + 1 : 0, to: start + items.length };
168}
169
170export type Facets = {
171 visibility: { public: number; private: number };
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97172 kind: Record<ProjectKind, number>;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar173 languages: { name: string; count: number }[];
174 deploying: number;
175 archived: number;
176};
177
178/** How many of the workspace's projects each filter would show, before any is chosen. */
179export function facetsOf(projects: Listed[]): Facets {
180 const languages = new Map<string, number>();
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.181 const facets: Facets = {
182 visibility: { public: 0, private: 0 },
183 kind: { app: 0, library: 0, tool: 0, docs: 0, other: 0 },
184 languages: [],
185 deploying: 0,
186 archived: 0,
187 };
Navigation: the workspace's own page with tabs, pins and recent in the sidebar188 for (const project of projects) {
189 facets.visibility[project.private ? "private" : "public"]++;
190 facets.kind[project.kind]++;
191 if (project.deploying) facets.deploying++;
192 if (project.archived) facets.archived++;
193 if (project.ecosystem) {
194 const language = LANGUAGE_OF[project.ecosystem];
195 languages.set(language, (languages.get(language) ?? 0) + 1);
196 }
197 }
198 facets.languages = [...languages].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
199 return facets;
200}
201
202/**
203 * The index of the row to move to from `at` by key, in a list of `count`
204 * rows: arrows and j/k move one, Home and End to either end. Null for any
205 * other key.
206 */
207export function nextRow(key: string, at: number, count: number): number | null {
208 if (count === 0) return null;
209 switch (key) {
210 case "ArrowDown":
211 case "j":
212 return Math.min(count - 1, at + 1);
213 case "ArrowUp":
214 case "k":
215 return Math.max(0, at - 1);
216 case "Home":
217 return 0;
218 case "End":
219 return count - 1;
220 default:
221 return null;
222 }
223}

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