Skip to content
840 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//! Teams: groups of a workspace's members, given roles on repositories
2//! together, mentioned together and asked to review together.
3//!
4//! **Who is in one.** A team has **maintainers**, who manage its people
5//! and settings, and **members**. Only members of the workspace can be in
6//! its teams; leaving the workspace takes a person out of all of them.
7//! Owners of the workspace manage every team, whether or not they are in
8//! it.
9//!
10//! **Visibility.** A **visible** team is seen by every member of the
11//! workspace. A **secret** team is seen only by its own people and the
12//! workspace's owners. Secret teams cannot be nested.
13//!
14//! **Nesting.** A team can have a parent. A child team inherits its
15//! parent's roles on repositories (and its parent's parent's), and a
16//! mention or review request for the parent reaches the child teams'
17//! people too. A team's own people never get anything from its children.
18//!
19//! **Repository access.** A team is given a [`RepoRole`] on a repository
20//! as a person is: a row in `repo_grants` whose principal is the team.
21//! Identity resolves it into the same [`RepoGrant`](crate::access::RepoGrant)s
22//! on every person in the team and in its child teams, so `access::can`
23//! decides with it as with any other grant: the highest role wins.
24//!
25//! **Review requests.** A pull request can ask a team to review it. With
26//! [`ReviewAssignment`] off, everyone in the team is asked. With it on,
27//! g1t picks `count` people from it (never the pull request's author) and
28//! asks them; the team stays shown as requested beside them.
29//!
30//! Every method is served by identity at `POST /rpc/<method>`. Changing a
31//! team is for people, signed in or with a personal access token; never
32//! an agent's or a workspace's token.
33
34use serde::{Deserialize, Serialize};
35
Merge branch 'worktree-agent-ad7c6d88d93adc817'36use crate::{Role, User};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar37use crate::access::RepoRole;
38use crate::repos::RepoPath;
39
40/// The most teams one workspace can have.
41pub const MAX_TEAMS: u32 = 500;
42/// The longest team name.
43pub const MAX_NAME_LENGTH: usize = 80;
44/// The longest description.
45pub const MAX_DESCRIPTION_LENGTH: usize = 280;
46/// How deep teams can nest: a team, its child, and so on.
47pub const MAX_DEPTH: usize = 8;
48/// The most people review assignment picks for one request.
49pub const MAX_ASSIGNED: u32 = 10;
50
51/// Who can see a team.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum TeamVisibility {
55 /// Every member of the workspace.
56 #[default]
57 Visible,
58 /// The team's own people and the workspace's owners.
59 Secret,
60}
61
62impl TeamVisibility {
63 pub fn as_str(self) -> &'static str {
64 match self {
65 TeamVisibility::Visible => "visible",
66 TeamVisibility::Secret => "secret",
67 }
68 }
69
70 pub fn parse(text: &str) -> Option<TeamVisibility> {
71 match text.trim().to_ascii_lowercase().as_str() {
72 "visible" | "closed" => Some(TeamVisibility::Visible),
73 "secret" => Some(TeamVisibility::Secret),
74 _ => None,
75 }
76 }
77}
78
Merge branch 'worktree-agent-ad7c6d88d93adc817'79/// Who may create a workspace's teams: a workspace setting, changed by
80/// its owners (`set_team_creation`). Stored in `workspaces.team_creation`,
81/// NULL for the default.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum TeamCreation {
85 /// Any member with a confirmed email address.
86 #[default]
87 Members,
88 /// The workspace's owners only.
89 Owners,
90}
91
92impl TeamCreation {
93 pub const ALL: [TeamCreation; 2] = [TeamCreation::Members, TeamCreation::Owners];
94
95 pub fn as_str(self) -> &'static str {
96 match self {
97 TeamCreation::Members => "members",
98 TeamCreation::Owners => "owners",
99 }
100 }
101
102 pub fn parse(text: &str) -> Option<TeamCreation> {
103 match text.trim().to_ascii_lowercase().as_str() {
104 "members" | "member" | "any" => Some(TeamCreation::Members),
105 "owners" | "owner" => Some(TeamCreation::Owners),
106 _ => None,
107 }
108 }
109
110 /// Whether someone with `role` in the workspace may create a team.
111 pub fn allows(self, role: Role) -> bool {
112 match self {
113 TeamCreation::Members => true,
114 TeamCreation::Owners => role == Role::Owner,
115 }
116 }
117}
118
119/// `set_team_creation`: who may create the workspace's teams. Owners
120/// only, as a person. Returns `Outcome<TeamCreation>`.
121#[derive(Debug, Serialize, Deserialize)]
122pub struct SetTeamCreationArgs {
123 pub actor: User,
124 pub slug: String,
125 pub team_creation: TeamCreation,
126 #[serde(default)]
127 pub surface: Option<crate::audit::Surface>,
128}
129
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar130/// A person's place in a team.
131#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum TeamRole {
134 Member,
135 /// Manages the team's people and settings.
136 Maintainer,
137}
138
139impl TeamRole {
140 pub fn as_str(self) -> &'static str {
141 match self {
142 TeamRole::Member => "member",
143 TeamRole::Maintainer => "maintainer",
144 }
145 }
146
147 pub fn parse(text: &str) -> Option<TeamRole> {
148 match text.trim().to_ascii_lowercase().as_str() {
149 "member" => Some(TeamRole::Member),
150 "maintainer" => Some(TeamRole::Maintainer),
151 _ => None,
152 }
153 }
154}
155
156/// How review assignment picks people.
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum ReviewAlgorithm {
160 /// Whoever was asked least recently by this team goes first.
161 #[default]
162 RoundRobin,
163 /// Whoever has the fewest pull requests waiting on their review goes
164 /// first.
165 LoadBalance,
166}
167
168impl ReviewAlgorithm {
169 pub fn as_str(self) -> &'static str {
170 match self {
171 ReviewAlgorithm::RoundRobin => "round_robin",
172 ReviewAlgorithm::LoadBalance => "load_balance",
173 }
174 }
175
176 pub fn parse(text: &str) -> Option<ReviewAlgorithm> {
177 match text.trim().to_ascii_lowercase().as_str() {
178 "round_robin" => Some(ReviewAlgorithm::RoundRobin),
179 "load_balance" => Some(ReviewAlgorithm::LoadBalance),
180 _ => None,
181 }
182 }
183}
184
185/// What happens when a team is asked to review a pull request.
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ReviewAssignment {
188 /// Off: everyone in the team is asked. On: `count` people are picked.
189 pub enabled: bool,
190 pub algorithm: ReviewAlgorithm,
191 /// How many people to pick, 1 to [`MAX_ASSIGNED`]. People from the team
192 /// already asked count towards it.
193 pub count: u32,
194 /// Leave out anyone with `busy_at` or more open pull requests waiting
195 /// on their review.
196 pub skip_busy: bool,
197 pub busy_at: u32,
198 /// Also pick from the people of its child teams.
199 pub include_child_teams: bool,
200 /// Usernames never picked.
201 #[serde(default)]
202 pub excluded: Vec<String>,
203 /// Also tell the rest of the team when people are picked.
204 pub notify_team: bool,
205}
206
207impl Default for ReviewAssignment {
208 fn default() -> Self {
209 ReviewAssignment {
210 enabled: false,
211 algorithm: ReviewAlgorithm::RoundRobin,
212 count: 1,
213 skip_busy: false,
214 busy_at: 5,
215 include_child_teams: false,
216 excluded: Vec::new(),
217 notify_team: false,
218 }
219 }
220}
221
222impl ReviewAssignment {
223 /// The same, with every number within bounds and the usernames
224 /// lowercased, each once.
225 pub fn bounded(mut self) -> Self {
226 self.count = self.count.clamp(1, MAX_ASSIGNED);
227 self.busy_at = self.busy_at.clamp(1, 100);
228 let mut excluded: Vec<String> = Vec::new();
229 for name in self.excluded {
230 let name = name.trim().trim_start_matches('@').to_lowercase();
231 if !name.is_empty() && !excluded.contains(&name) {
232 excluded.push(name);
233 }
234 }
235 excluded.truncate(100);
236 self.excluded = excluded;
237 self
238 }
239}
240
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.241/// Who leads a team: a person on it, or an agent on it.
242#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(tag = "kind", rename_all = "snake_case")]
244pub enum TeamLead {
245 User {
246 username: String,
247 #[serde(default)]
248 name: Option<String>,
249 #[serde(default)]
250 avatar: Option<String>,
251 },
252 /// An agent, by its id in the agents service.
253 Agent { agent_id: String },
254}
255
256/// A lead as written to `update_team`: `@username` (or a bare username)
257/// for a person, `agent:<id>` for an agent.
258#[derive(Clone, Debug, PartialEq, Eq)]
259pub enum LeadInput {
260 User(String),
261 Agent(String),
262}
263
264/// `lead` as `update_team` takes it; `None` when it is not one. An empty
265/// string means no lead, and is not parsed here.
266pub fn parse_lead(text: &str) -> Option<LeadInput> {
267 let text = text.trim();
268 if let Some(id) = text.strip_prefix("agent:") {
269 let id = id.trim();
270 let ok = !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
271 return ok.then(|| LeadInput::Agent(id.to_owned()));
272 }
273 let name = text.trim_start_matches('@').to_lowercase();
274 crate::is_valid_namespace(&name).then_some(LeadInput::User(name))
275}
276
277/// A team's chat channel.
278#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
279pub struct TeamChannel {
280 /// Its id in the chat service.
281 pub id: String,
282 /// Its name, without `#`, as it was when chosen.
283 pub name: String,
284}
285
286/// One agent on a team.
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct TeamAgent {
289 pub agent_id: String,
290 /// Who added it, by username; null when its account is gone.
291 pub added_by: Option<String>,
292 /// RFC 3339.
293 pub created_at: String,
294}
295
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar296/// A team as another names it: its parent, or a child.
297#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
298pub struct TeamRef {
299 pub slug: String,
300 pub name: String,
301}
302
303#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
304pub struct Team {
305 pub id: String,
306 /// Its workspace's slug.
307 pub workspace: String,
308 /// Its name in URLs and mentions: `@<workspace>/<slug>`.
309 pub slug: String,
310 pub name: String,
311 pub description: Option<String>,
312 pub visibility: TeamVisibility,
313 pub parent: Option<TeamRef>,
314 /// Whether its people are notified when it is mentioned.
315 pub notify: bool,
316 pub review_assignment: ReviewAssignment,
317 /// Its own people, not counting child teams'.
318 pub members_count: u32,
319 /// Repositories it has a role on itself, not counting inherited ones.
320 pub repos_count: u32,
321 pub child_teams_count: u32,
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.322 /// Agents added to it. Agents whose home team it is are on it too.
323 #[serde(default)]
324 pub agents_count: u32,
325 /// Who leads it, if anyone.
326 #[serde(default)]
327 pub lead: Option<TeamLead>,
328 /// Its chat channel, if it has one.
329 #[serde(default)]
330 pub channel: Option<TeamChannel>,
331 /// What its agents may spend together in a calendar month, in
332 /// millionths of a dollar; null for no team budget.
333 #[serde(default)]
334 pub budget_micros: Option<i64>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar335 /// The viewer's place in it, if any.
336 pub viewer_role: Option<TeamRole>,
337 /// Whether the viewer may change it: an owner of the workspace, or one
338 /// of its maintainers.
339 pub can_manage: bool,
340 /// RFC 3339.
341 pub created_at: String,
342 pub updated_at: String,
343}
344
345impl Team {
346 /// How it is mentioned: `@acme/backend`.
347 pub fn handle(&self) -> String {
348 format!("@{}/{}", self.workspace, self.slug)
349 }
350}
351
352/// One person in a team.
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct TeamMember {
355 pub username: String,
356 pub name: Option<String>,
357 pub avatar: Option<String>,
358 pub role: TeamRole,
359 /// The child team they are in, when listed through one; null for the
360 /// team's own people.
361 pub via: Option<String>,
362}
363
364/// A repository a team has a role on.
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
366pub struct TeamRepo {
367 /// `workspace/name`.
368 pub repo: String,
369 pub repo_id: String,
370 pub role: RepoRole,
371 /// The parent team it comes from, by slug, when the team inherits it.
372 pub inherited_from: Option<String>,
373}
374
375/// A team with a role on a repository, as its Access settings list it.
376#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
377pub struct RepoTeam {
378 pub slug: String,
379 pub name: String,
380 pub role: RepoRole,
381 pub members_count: u32,
382 pub visibility: TeamVisibility,
383}
384
385/// A team as services need it to notify or ask its people: everyone in it,
386/// and its settings. Never shown as is.
387#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
388pub struct ResolvedTeam {
389 pub id: String,
390 pub workspace: String,
391 pub slug: String,
392 pub name: String,
393 pub visibility: TeamVisibility,
394 pub notify: bool,
395 pub review_assignment: ReviewAssignment,
396 /// Its own people.
397 pub members: Vec<TeamPerson>,
398 /// The people of its child teams (and theirs) who are not its own.
399 pub child_members: Vec<TeamPerson>,
400 /// Its role on the repository asked about, its own or inherited.
401 pub repo_role: Option<RepoRole>,
402 /// Whether the person asked about (`asker`) may see it, and so mention
403 /// it or ask it to review: a member of its workspace, and for a secret
404 /// team, in it or an owner.
405 #[serde(default)]
406 pub asker_sees: bool,
407}
408
409impl ResolvedTeam {
410 /// Everyone a mention or a request reaches: its own people, then its
411 /// child teams'.
412 pub fn everyone(&self) -> impl Iterator<Item = &TeamPerson> {
413 self.members.iter().chain(self.child_members.iter())
414 }
415
416 pub fn handle(&self) -> String {
417 format!("@{}/{}", self.workspace, self.slug)
418 }
419}
420
421#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
422pub struct TeamPerson {
423 pub id: String,
424 pub username: String,
425}
426
427/// A team's slug from its name: lowercase letters, digits and single
428/// hyphens, as mentions spell it. `None` when nothing is left.
429pub fn slug_of(name: &str) -> Option<String> {
430 let mut slug = String::new();
431 for c in name.trim().chars() {
432 if c.is_ascii_alphanumeric() {
433 slug.push(c.to_ascii_lowercase());
434 } else if !slug.is_empty() && !slug.ends_with('-') {
435 slug.push('-');
436 }
437 }
438 let slug = slug.trim_end_matches('-').chars().take(60).collect::<String>();
439 let slug = slug.trim_end_matches('-').to_owned();
440 is_valid_slug(&slug).then_some(slug)
441}
442
443/// Whether `slug` is a team slug: 1 to 60 lowercase letters, digits and
444/// single hyphens, not starting or ending with one.
445pub fn is_valid_slug(slug: &str) -> bool {
446 !slug.is_empty()
447 && slug.len() <= 60
448 && slug.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
449 && !slug.starts_with('-')
450 && !slug.ends_with('-')
451 && !slug.contains("--")
452}
453
454/// `@workspace/team` as written, split; `None` if it is not that shape.
455pub fn parse_handle(text: &str) -> Option<(String, String)> {
456 let text = text.trim().strip_prefix('@')?;
457 let (workspace, slug) = text.split_once('/')?;
458 let workspace = workspace.to_lowercase();
459 let slug = slug.to_lowercase();
460 (crate::is_valid_namespace(&workspace) && is_valid_slug(&slug)).then_some((workspace, slug))
461}
462
463// --- Identity methods ---------------------------------------------------------
464
465/// `list_teams`: the teams of a workspace the viewer can see, theirs first,
466/// then by name. Members only. `query` narrows by name or slug. Returns
467/// `Outcome<Vec<Team>>`.
468#[derive(Debug, Serialize, Deserialize)]
469pub struct ListTeamsArgs {
470 pub viewer: crate::Viewer,
471 pub workspace: String,
472 #[serde(default)]
473 pub query: Option<String>,
474}
475
476/// `get_team`, `child_teams`, `team_repos`: one team, its child teams, or
477/// the repositories it has a role on (its own and inherited), as the
478/// viewer may see them. `team_members` takes `include_child_teams`.
479#[derive(Debug, Serialize, Deserialize)]
480pub struct TeamArgs {
481 pub viewer: crate::Viewer,
482 pub workspace: String,
483 pub team: String,
484 /// `team_members` only: also list the people of child teams.
485 #[serde(default)]
486 pub include_child_teams: bool,
487}
488
Merge branch 'worktree-agent-ad7c6d88d93adc817'489/// `create_team`. Members may create a team, unless the workspace's
490/// [`TeamCreation`] says owners only, and become its maintainer; a team
491/// with a parent needs an owner, or a maintainer of the parent. Returns
492/// `Outcome<Team>`.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar493#[derive(Debug, Serialize, Deserialize)]
494pub struct CreateTeamArgs {
495 pub actor: User,
496 pub workspace: String,
497 pub name: String,
498 /// Defaults to one made from the name.
499 #[serde(default)]
500 pub slug: Option<String>,
501 #[serde(default)]
502 pub description: Option<String>,
503 #[serde(default)]
504 pub visibility: Option<TeamVisibility>,
505 /// The parent's slug.
506 #[serde(default)]
507 pub parent: Option<String>,
508 #[serde(default)]
509 pub notify: Option<bool>,
510 /// People to add as members, by username, besides the creator.
511 #[serde(default)]
512 pub members: Vec<String>,
513 #[serde(default)]
514 pub surface: Option<crate::audit::Surface>,
515}
516
517/// `update_team`: what is given changes; the rest stays. `parent` set to an
518/// empty string takes the team out from under its parent. Owners and the
519/// team's maintainers. Returns `Outcome<Team>`.
520#[derive(Debug, Default, Serialize, Deserialize)]
521pub struct UpdateTeamArgs {
522 pub actor: User,
523 pub workspace: String,
524 pub team: String,
525 #[serde(default)]
526 pub name: Option<String>,
527 #[serde(default)]
528 pub slug: Option<String>,
529 #[serde(default)]
530 pub description: Option<String>,
531 #[serde(default)]
532 pub visibility: Option<TeamVisibility>,
533 #[serde(default)]
534 pub parent: Option<String>,
535 #[serde(default)]
536 pub notify: Option<bool>,
537 #[serde(default)]
538 pub review_assignment: Option<ReviewAssignment>,
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.539 /// Who leads it: `@username` or `agent:<id>`, someone on the team; an
540 /// empty string for no lead.
541 #[serde(default)]
542 pub lead: Option<String>,
543 /// Its chat channel's id in the chat service; an empty string for none.
544 #[serde(default)]
545 pub channel_id: Option<String>,
546 /// That channel's name, without `#`; given with `channel_id`.
547 #[serde(default)]
548 pub channel_name: Option<String>,
549 /// What its agents may spend together in a month, in millionths of a
550 /// dollar; 0 for no team budget.
551 #[serde(default)]
552 pub budget_micros: Option<i64>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar553 #[serde(default)]
554 pub surface: Option<crate::audit::Surface>,
555}
556
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.557/// `set_team_agent`: adds one of the workspace's agents to a team. The
558/// caller checks the agent is the workspace's (identity knows agents only
559/// by id). Owners and the team's maintainers. Returns `Outcome<TeamAgent>`.
560#[derive(Debug, Serialize, Deserialize)]
561pub struct SetTeamAgentArgs {
562 pub actor: User,
563 pub workspace: String,
564 pub team: String,
565 pub agent_id: String,
566 #[serde(default)]
567 pub surface: Option<crate::audit::Surface>,
568}
569
570/// `remove_team_agent`: takes an agent off a team; if it led the team,
571/// the team has no lead after. Owners and the team's maintainers.
572/// Returns `Outcome<bool>`.
573#[derive(Debug, Serialize, Deserialize)]
574pub struct RemoveTeamAgentArgs {
575 pub actor: User,
576 pub workspace: String,
577 pub team: String,
578 pub agent_id: String,
579 #[serde(default)]
580 pub surface: Option<crate::audit::Surface>,
581}
582
583/// `agent_teams`: for the agents service. The visible teams an agent is
584/// on in a workspace (added to, or its home team), each with everyone on
585/// it, for what the agent is told every turn and its team budgets.
586/// Returns `Vec<AgentTeam>`.
587#[derive(Debug, Default, Serialize, Deserialize)]
588pub struct AgentTeamsArgs {
589 /// The workspace's slug.
590 pub workspace: String,
591 pub agent_id: String,
592 /// The team its profile names, by slug.
593 #[serde(default)]
594 pub home_team: Option<String>,
595}
596
597/// A person on a team, as an agent on it is told of them.
598#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
599pub struct RosterPerson {
600 pub user_id: String,
601 pub username: String,
602 pub name: Option<String>,
603 pub title: Option<String>,
604 pub timezone: Option<String>,
605 pub owns: Vec<String>,
606 /// Who they report to, by username.
607 pub manager: Option<String>,
608 pub maintainer: bool,
609}
610
611/// A team an agent is on, as it is told of it.
612#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
613pub struct AgentTeam {
614 pub slug: String,
615 pub name: String,
616 pub description: Option<String>,
617 pub lead: Option<TeamLead>,
618 pub channel: Option<TeamChannel>,
619 pub budget_micros: Option<i64>,
620 pub people: Vec<RosterPerson>,
621 /// The agents added to it, by id.
622 pub agent_ids: Vec<String>,
623}
624
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar625/// `delete_team`: its child teams move up to its parent, and the roles it
626/// gave go with it. Owners and the team's maintainers. Returns
627/// `Outcome<bool>`.
628#[derive(Debug, Serialize, Deserialize)]
629pub struct DeleteTeamArgs {
630 pub actor: User,
631 pub workspace: String,
632 pub team: String,
633 #[serde(default)]
634 pub surface: Option<crate::audit::Surface>,
635}
636
637/// `set_team_member`: adds a member of the workspace to a team, or changes
638/// their role in it. Owners and the team's maintainers. Returns
639/// `Outcome<TeamMember>`.
640#[derive(Debug, Serialize, Deserialize)]
641pub struct SetTeamMemberArgs {
642 pub actor: User,
643 pub workspace: String,
644 pub team: String,
645 pub username: String,
646 pub role: TeamRole,
647 #[serde(default)]
648 pub surface: Option<crate::audit::Surface>,
649}
650
651/// `remove_team_member`: owners and the team's maintainers; anyone may
652/// leave a team themselves. Returns `Outcome<bool>`.
653#[derive(Debug, Serialize, Deserialize)]
654pub struct RemoveTeamMemberArgs {
655 pub actor: User,
656 pub workspace: String,
657 pub team: String,
658 pub username: String,
659 #[serde(default)]
660 pub surface: Option<crate::audit::Surface>,
661}
662
663/// `set_team_repo`: gives a team a role on a repository of its workspace,
664/// or changes it. Needs Admin on the repository. Returns
665/// `Outcome<TeamRepo>`.
666#[derive(Debug, Serialize, Deserialize)]
667pub struct SetTeamRepoArgs {
668 pub actor: User,
669 pub workspace: String,
670 pub team: String,
671 pub repo: RepoPath,
672 pub role: RepoRole,
673 #[serde(default)]
674 pub surface: Option<crate::audit::Surface>,
675}
676
677/// `remove_team_repo`: takes a team's role on a repository away. Admin on
678/// the repository, an owner, or one of the team's maintainers. Returns
679/// `Outcome<bool>`.
680#[derive(Debug, Serialize, Deserialize)]
681pub struct RemoveTeamRepoArgs {
682 pub actor: User,
683 pub workspace: String,
684 pub team: String,
685 pub repo: RepoPath,
686 #[serde(default)]
687 pub surface: Option<crate::audit::Surface>,
688}
689
690/// `user_teams`: the teams `username` is in within a workspace, as the
691/// viewer may see them. Members only. Returns `Outcome<Vec<Team>>`.
692#[derive(Debug, Serialize, Deserialize)]
693pub struct UserTeamsArgs {
694 pub viewer: crate::Viewer,
695 pub workspace: String,
696 pub username: String,
697}
698
699/// `team_memberships`: for each member of a workspace, the teams they are
700/// in that the viewer can see, for the Members page. Members only.
701/// Returns `Outcome<Vec<MemberTeams>>`.
702#[derive(Debug, Serialize, Deserialize)]
703pub struct TeamMembershipsArgs {
704 pub viewer: crate::Viewer,
705 pub workspace: String,
706}
707
708/// One person's teams in a workspace.
709#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
710pub struct MemberTeams {
711 pub username: String,
712 pub teams: Vec<TeamRef>,
713}
714
715/// `resolve_teams`: for services. Each team named `workspace/slug` (or
716/// `@workspace/slug`) that exists, with everyone in it and, given
717/// `repo_id`, its role on that repository. Missing teams are left out.
718/// Returns `Vec<ResolvedTeam>`.
719#[derive(Debug, Default, Serialize, Deserialize)]
720pub struct ResolveTeamsArgs {
721 pub teams: Vec<String>,
722 #[serde(default)]
723 pub repo_id: Option<String>,
724 /// A user id, for `asker_sees`.
725 #[serde(default)]
726 pub asker: Option<String>,
727}
728
729/// One owner a CODEOWNERS file names, as identity resolved it.
730#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
731pub struct ResolvedOwner {
732 pub owner: crate::codeowners::Owner,
733 pub check: crate::codeowners::OwnerCheck,
734 /// Who may answer for it, by username: the person, the account a
735 /// confirmed address belongs to, or everyone in a team and its child
736 /// teams. Empty when it did not resolve.
737 pub members: Vec<String>,
738 /// For a team: the team, as `workspace/slug`, after `@org/team` was
739 /// mapped to the repository's workspace.
740 #[serde(default)]
741 pub team: Option<String>,
742}
743
744/// `resolve_owners`: for services. Resolves the owners a CODEOWNERS file
745/// names on a repository: accounts, teams of the repository's workspace
746/// (`@org/team` with an `org` that is not a g1t workspace means the
747/// repository's own workspace), and confirmed email addresses, checking
748/// each has the Write role or higher on it. `g1t` resolves to itself.
749/// Returns `Vec<ResolvedOwner>`, in the order asked.
750#[derive(Debug, Serialize, Deserialize)]
751pub struct ResolveOwnersArgs {
752 pub repo_id: String,
753 /// The repository's workspace, by slug.
754 pub workspace: String,
755 pub owners: Vec<crate::codeowners::Owner>,
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
Merge branch 'worktree-agent-ad7c6d88d93adc817'763 fn who_may_create_teams() {
764 assert_eq!(TeamCreation::default(), TeamCreation::Members);
765 assert!(TeamCreation::Members.allows(Role::Member) && TeamCreation::Members.allows(Role::Owner));
766 assert!(!TeamCreation::Owners.allows(Role::Member) && TeamCreation::Owners.allows(Role::Owner));
767 for setting in TeamCreation::ALL {
768 assert_eq!(TeamCreation::parse(setting.as_str()), Some(setting));
769 assert_eq!(serde_json::to_value(setting).unwrap(), setting.as_str());
770 }
771 assert_eq!(TeamCreation::parse(" Owners "), Some(TeamCreation::Owners));
772 assert_eq!(TeamCreation::parse("maintainers"), None);
773 }
774
775 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar776 fn slugs_come_from_names() {
777 assert_eq!(slug_of("Backend").as_deref(), Some("backend"));
778 assert_eq!(slug_of(" Web & Mobile ").as_deref(), Some("web-mobile"));
779 assert_eq!(slug_of("SRE / On-call").as_deref(), Some("sre-on-call"));
780 assert_eq!(slug_of("!!!"), None);
781 assert_eq!(slug_of(&"a".repeat(80)).map(|slug| slug.len()), Some(60));
782 assert!(is_valid_slug("platform-2"));
783 assert!(!is_valid_slug("Platform") && !is_valid_slug("-a") && !is_valid_slug("a--b") && !is_valid_slug(""));
784 }
785
786 #[test]
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.787 fn leads_are_people_or_agents() {
788 assert_eq!(parse_lead("@Priya"), Some(LeadInput::User("priya".into())));
789 assert_eq!(parse_lead("priya"), Some(LeadInput::User("priya".into())));
790 assert_eq!(parse_lead("agent:agt_123"), Some(LeadInput::Agent("agt_123".into())));
791 assert_eq!(parse_lead("agent:"), None);
792 assert_eq!(parse_lead("agent:a b"), None);
793 assert_eq!(parse_lead("not a name"), None);
794 let lead = TeamLead::Agent { agent_id: "agt_1".into() };
795 assert_eq!(serde_json::to_value(&lead).unwrap(), serde_json::json!({ "kind": "agent", "agent_id": "agt_1" }));
796 let lead: TeamLead = serde_json::from_value(serde_json::json!({ "kind": "user", "username": "ana" })).unwrap();
797 assert_eq!(lead, TeamLead::User { username: "ana".into(), name: None, avatar: None });
798 }
799
800 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar801 fn handles_name_a_workspace_and_a_team() {
802 assert_eq!(parse_handle("@acme/backend"), Some(("acme".into(), "backend".into())));
803 assert_eq!(parse_handle("@Acme/Backend"), Some(("acme".into(), "backend".into())));
804 assert_eq!(parse_handle("acme/backend"), None);
805 assert_eq!(parse_handle("@acme"), None);
806 assert_eq!(parse_handle("@acme/a/b"), None);
807 }
808
809 #[test]
810 fn words_read_back() {
811 for visibility in [TeamVisibility::Visible, TeamVisibility::Secret] {
812 assert_eq!(TeamVisibility::parse(visibility.as_str()), Some(visibility));
813 assert_eq!(serde_json::to_value(visibility).unwrap(), visibility.as_str());
814 }
815 for role in [TeamRole::Member, TeamRole::Maintainer] {
816 assert_eq!(TeamRole::parse(role.as_str()), Some(role));
817 assert_eq!(serde_json::to_value(role).unwrap(), role.as_str());
818 }
819 for algorithm in [ReviewAlgorithm::RoundRobin, ReviewAlgorithm::LoadBalance] {
820 assert_eq!(ReviewAlgorithm::parse(algorithm.as_str()), Some(algorithm));
821 assert_eq!(serde_json::to_value(algorithm).unwrap(), algorithm.as_str());
822 }
823 assert!(TeamRole::Member < TeamRole::Maintainer);
824 }
825
826 #[test]
827 fn review_assignment_is_kept_within_bounds() {
828 let wild = ReviewAssignment {
829 count: 0,
830 busy_at: 0,
831 excluded: vec!["@Ana".into(), "ana".into(), " ".into(), "bo".into()],
832 ..ReviewAssignment::default()
833 }
834 .bounded();
835 assert_eq!(wild.count, 1);
836 assert_eq!(wild.busy_at, 1);
837 assert_eq!(wild.excluded, vec!["ana", "bo"]);
838 assert_eq!(ReviewAssignment { count: 50, ..ReviewAssignment::default() }.bounded().count, MAX_ASSIGNED);
839 }
840}

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