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

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1/**
2 * Teams on the site: finding them, and reading their forms. Types only from
3 * the contracts, so this file runs under `node --test`.
4 */
5import type { NewTeam, ReviewAlgorithm, ReviewAssignment, Team, TeamChanges, TeamVisibility } from "@g1t/contracts";
6
7/** The most people review assignment picks; the same as the contracts' `MAX_ASSIGNED`. */
8const MAX_ASSIGNED = 10;
9
10/** Where a team's pages are. */
11export function teamPath(workspace: string, slug: string, tab?: "teams" | "repositories" | "settings"): string {
12 return `/${workspace}/-/teams/${slug}${tab ? `/${tab}` : ""}`;
13}
14
15/** The teams whose name, slug or description has every word of `query`, in their order. */
16export function filterTeams<T extends Pick<Team, "name" | "slug" | "description">>(teams: T[], query: string): T[] {
17 const words = query.toLowerCase().split(/\s+/).filter(Boolean);
18 if (words.length === 0) return teams;
19 return teams.filter((team) => {
20 const text = `${team.name} ${team.slug} ${team.description ?? ""}`.toLowerCase();
21 return words.every((word) => text.includes(word));
22 });
23}
24
25/** The viewer's teams, then the rest, keeping the order given. */
26export function splitTeams<T extends Pick<Team, "viewer_role">>(teams: T[]): { mine: T[]; others: T[] } {
27 return {
28 mine: teams.filter((team) => team.viewer_role != null),
29 others: teams.filter((team) => team.viewer_role == null),
30 };
31}
32
33/** The teams `team` could go under: visible, not itself, and not one of its own children. */
34export function parentChoices<T extends Pick<Team, "slug" | "visibility" | "parent">>(teams: T[], team: string | null): T[] {
35 if (!team) return teams.filter((other) => other.visibility === "visible");
36 // Everything under `team`, by following parents.
37 const below = new Set<string>([team]);
38 let grew = true;
39 while (grew) {
40 grew = false;
41 for (const other of teams) {
42 if (other.parent && below.has(other.parent.slug) && !below.has(other.slug)) {
43 below.add(other.slug);
44 grew = true;
45 }
46 }
47 }
48 return teams.filter((other) => other.visibility === "visible" && !below.has(other.slug));
49}
50
51const text = (form: FormData, key: string) => String(form.get(key) ?? "").trim();
52
53function visibilityOf(value: string): TeamVisibility | undefined {
54 return value === "secret" || value === "visible" ? value : undefined;
55}
56
57/** A new team from the form on the New team page. */
58export function newTeamFromForm(form: FormData): NewTeam {
59 const members = text(form, "members")
60 .split(/[\s,]+/)
61 .map((name) => name.replace(/^@/, ""))
62 .filter(Boolean);
63 return {
64 name: text(form, "name"),
65 slug: text(form, "slug") || null,
66 description: text(form, "description") || null,
67 visibility: visibilityOf(text(form, "visibility")) ?? "visible",
68 parent: text(form, "parent") || null,
69 members,
70 };
71}
72
73/** What the team's settings form changes: only the fields it has. */
74export function teamChangesFromForm(form: FormData): TeamChanges {
75 const changes: TeamChanges = {};
76 if (form.has("name")) changes.name = text(form, "name");
77 if (form.has("slug")) changes.slug = text(form, "slug");
78 if (form.has("description")) changes.description = text(form, "description");
79 const visibility = visibilityOf(text(form, "visibility"));
80 if (visibility) changes.visibility = visibility;
81 // "" takes it out from under its parent.
82 if (form.has("parent")) changes.parent = text(form, "parent");
83 if (form.has("notify-shown")) changes.notify = form.get("notify") != null;
84 return changes;
85}
86
87function count(value: string, fallback: number, low: number, high: number): number {
88 const number = Number.parseInt(value, 10);
89 if (!Number.isFinite(number)) return fallback;
90 return Math.min(high, Math.max(low, number));
91}
92
93/** Review assignment from its form, within bounds; `current` fills what the form leaves out. */
94export function reviewAssignmentFromForm(form: FormData, current: ReviewAssignment): ReviewAssignment {
95 const algorithm = text(form, "algorithm");
96 return {
97 enabled: form.get("enabled") != null,
98 algorithm: (algorithm === "round_robin" || algorithm === "load_balance" ? algorithm : current.algorithm) as ReviewAlgorithm,
99 count: count(text(form, "count"), current.count, 1, MAX_ASSIGNED),
100 skip_busy: form.get("skip_busy") != null,
101 busy_at: count(text(form, "busy_at"), current.busy_at, 1, 100),
102 include_child_teams: form.get("include_child_teams") != null,
103 excluded: [
104 ...new Set(
105 text(form, "excluded")
106 .split(/[\s,]+/)
107 .map((name) => name.replace(/^@/, "").toLowerCase())
108 .filter(Boolean),
109 ),
110 ],
111 notify_team: form.get("notify_team") != null,
112 };
113}
114
115/** "3 members · 2 repositories · 1 child team", leaving out what is none. */
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.116export function teamCounts(team: Pick<Team, "members_count" | "repos_count" | "child_teams_count">): string {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar117 const part = (n: number, one: string, many: string) => (n === 0 ? null : `${n} ${n === 1 ? one : many}`);
118 const parts = [
119 part(team.members_count, "member", "members"),
120 part(team.repos_count, "repository", "repositories"),
121 part(team.child_teams_count, "child team", "child teams"),
122 ].filter(Boolean);
123 return parts.length ? parts.join(" · ") : "No members yet";
124}