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
202 linesCodeBlameRaw
1/**
2 * Teams: groups of a workspace's members, given roles on repositories
3 * together, mentioned together and asked to review together. Kept by the
4 * identity service; mirrors `crates/contracts/src/teams.rs`.
5 */
6import type { RepoRole } from "./access";
7import type { User } from "./identity";
8import type { Result } from "./result";
9
10export type TeamVisibility = "visible" | "secret";
11
12/**
13 * Who may create a workspace's teams: any member (the default) or its
14 * owners only. Owners change it in the workspace's settings.
15 */
16export type TeamCreation = "members" | "owners";
17
18/** Whether someone with `role` in a workspace may create its teams. */
19export function mayCreateTeams(setting: TeamCreation | null | undefined, role: "owner" | "member" | null | undefined): boolean {
20 if (!role) return false;
21 return (setting ?? "members") === "members" || role === "owner";
22}
23export type TeamRole = "member" | "maintainer";
24export type ReviewAlgorithm = "round_robin" | "load_balance";
25
26export const TEAM_VISIBILITY_LABELS: Record<TeamVisibility, string> = {
27 visible: "Visible",
28 secret: "Secret",
29};
30
31export const TEAM_VISIBILITY_SUMMARIES: Record<TeamVisibility, string> = {
32 visible: "Every member of the workspace can see it and mention it.",
33 secret: "Only its own people and the workspace's owners can see it. Secret teams cannot be nested.",
34};
35
36export const REVIEW_ALGORITHM_LABELS: Record<ReviewAlgorithm, string> = {
37 round_robin: "Round robin",
38 load_balance: "Load balance",
39};
40
41export const REVIEW_ALGORITHM_SUMMARIES: Record<ReviewAlgorithm, string> = {
42 round_robin: "Whoever this team asked least recently goes first.",
43 load_balance: "Whoever has the fewest pull requests waiting on their review goes first.",
44};
45
46/** The most people review assignment picks for one request. */
47export const MAX_ASSIGNED = 10;
48
49export type ReviewAssignment = {
50 enabled: boolean;
51 algorithm: ReviewAlgorithm;
52 count: number;
53 skip_busy: boolean;
54 busy_at: number;
55 include_child_teams: boolean;
56 excluded: string[];
57 notify_team: boolean;
58};
59
60export const DEFAULT_REVIEW_ASSIGNMENT: ReviewAssignment = {
61 enabled: false,
62 algorithm: "round_robin",
63 count: 1,
64 skip_busy: false,
65 busy_at: 5,
66 include_child_teams: false,
67 excluded: [],
68 notify_team: false,
69};
70
71export type TeamRef = { slug: string; name: string };
72
73/**
74 * Who leads a team: a person on it. `agent` is a lead from g1t's retired
75 * hosted agents, kept only so old data parses; identity never answers with
76 * one and no team can be given one.
77 */
78export type TeamLead =
79 | { kind: "user"; username: string; name: string | null; avatar: string | null }
80 | { kind: "agent"; agent_id: string };
81
82/** A team's chat channel: its id, and its name (without `#`) when it was chosen. */
83export type TeamChannel = { id: string; name: string };
84
85/** How `update_team` takes a lead: `@username`; `""` for none. */
86export function leadInput(lead: { username: string } | null): string {
87 return lead ? `@${lead.username}` : "";
88}
89
90export type Team = {
91 id: string;
92 workspace: string;
93 slug: string;
94 name: string;
95 description: string | null;
96 visibility: TeamVisibility;
97 parent: TeamRef | null;
98 notify: boolean;
99 review_assignment: ReviewAssignment;
100 members_count: number;
101 repos_count: number;
102 child_teams_count: number;
103 lead: TeamLead | null;
104 channel: TeamChannel | null;
105 viewer_role: TeamRole | null;
106 can_manage: boolean;
107 created_at: string;
108 updated_at: string;
109};
110
111export type TeamMember = {
112 username: string;
113 name: string | null;
114 avatar: string | null;
115 role: TeamRole;
116 /** The child team they are in, when listed through one. */
117 via: string | null;
118};
119
120export type TeamRepo = {
121 /** `workspace/name`. */
122 repo: string;
123 repo_id: string;
124 role: RepoRole;
125 /** The parent team it comes from, when inherited. */
126 inherited_from: string | null;
127};
128
129/** A team with a role on a repository, as its Access settings list it. */
130export type RepoTeam = {
131 slug: string;
132 name: string;
133 role: RepoRole;
134 members_count: number;
135 visibility: TeamVisibility;
136};
137
138export type NewTeam = {
139 name: string;
140 slug?: string | null;
141 description?: string | null;
142 visibility?: TeamVisibility | null;
143 parent?: string | null;
144 notify?: boolean | null;
145 members?: string[];
146};
147
148/** What changes; the rest stays. `parent: ""` takes the team out from under its parent. */
149export type TeamChanges = {
150 name?: string;
151 slug?: string;
152 description?: string;
153 visibility?: TeamVisibility;
154 parent?: string;
155 notify?: boolean;
156 review_assignment?: ReviewAssignment;
157 /** `@username`, someone on the team (see `leadInput`); `""` for no lead. */
158 lead?: string;
159 /** The chat channel's id; `""` for none. Given with `channel_name`. */
160 channel_id?: string;
161 channel_name?: string;
162};
163
164/** `@acme/backend`, the way a team is mentioned. */
165export function teamHandle(team: { workspace: string; slug: string }): string {
166 return `@${team.workspace}/${team.slug}`;
167}
168
169/** A team's slug from its name, as identity makes it. Null when nothing is left. */
170export function teamSlug(name: string): string | null {
171 let slug = "";
172 for (const c of name.trim()) {
173 if (/[A-Za-z0-9]/.test(c)) slug += c.toLowerCase();
174 else if (slug && !slug.endsWith("-")) slug += "-";
175 }
176 slug = slug.replace(/-+$/, "").slice(0, 60).replace(/-+$/, "");
177 return slug ? slug : null;
178}
179
180/** One person's teams in a workspace, for the Members page. */
181export type MemberTeams = { username: string; teams: TeamRef[] };
182
183/** Identity's team methods. */
184export interface TeamsClient {
185 listTeams(viewer: User | null, workspace: string, query?: string | null): Promise<Result<Team[]>>;
186 getTeam(viewer: User | null, workspace: string, team: string): Promise<Result<Team>>;
187 createTeam(actor: User, workspace: string, team: NewTeam): Promise<Result<Team>>;
188 /** Who may create the workspace's teams. Owners only. */
189 setTeamCreation(actor: User, slug: string, setting: TeamCreation): Promise<Result<TeamCreation>>;
190 updateTeam(actor: User, workspace: string, team: string, changes: TeamChanges): Promise<Result<Team>>;
191 deleteTeam(actor: User, workspace: string, team: string): Promise<Result<boolean>>;
192 teamMembers(viewer: User | null, workspace: string, team: string, includeChildTeams?: boolean): Promise<Result<TeamMember[]>>;
193 setTeamMember(actor: User, workspace: string, team: string, username: string, role: TeamRole): Promise<Result<TeamMember>>;
194 removeTeamMember(actor: User, workspace: string, team: string, username: string): Promise<Result<boolean>>;
195 childTeams(viewer: User | null, workspace: string, team: string): Promise<Result<Team[]>>;
196 teamRepos(viewer: User | null, workspace: string, team: string): Promise<Result<TeamRepo[]>>;
197 setTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string, role: RepoRole): Promise<Result<TeamRepo>>;
198 removeTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string): Promise<Result<boolean>>;
199 userTeams(viewer: User | null, workspace: string, username: string): Promise<Result<Team[]>>;
200 /** Each member's teams the viewer can see. Members only. */
201 teamMemberships(viewer: User | null, workspace: string): Promise<Result<MemberTeams[]>>;
202}