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
183 linesCodeBlameRaw
1//! People: a workspace's directory of the people in it, and what each
2//! person's place is: a title and what they own. Kept by identity beside
3//! membership.
4//!
5//! **Profiles.** A member's title and what they own are theirs to write,
6//! and the workspace's owners can write anyone's.
7//!
8//! Every method is served by identity at `POST /rpc/<method>`.
9
10use serde::{Deserialize, Serialize};
11
12use crate::teams::{TeamChannel, TeamLead, TeamRef, TeamRole, TeamVisibility};
13use crate::{Role, User};
14
15/// The longest title.
16pub const MAX_TITLE_LENGTH: usize = 80;
17/// The most things one person owns, as their profile lists them.
18pub const MAX_OWNS: usize = 8;
19/// The longest of them.
20pub const MAX_OWNS_LENGTH: usize = 60;
21
22/// One member, as the directory and their profile show them.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub struct DirectoryPerson {
25 pub user_id: String,
26 pub username: String,
27 /// The username as its owner wrote it, when that differs.
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub display_username: Option<String>,
30 pub name: Option<String>,
31 pub avatar: Option<String>,
32 /// From their public profile.
33 pub bio: Option<String>,
34 pub location: Option<String>,
35 pub pronouns: Option<String>,
36 /// An IANA name, such as `America/Denver`, for their local time.
37 pub timezone: Option<String>,
38 /// Owner or member.
39 pub role: Role,
40 /// Their title in this workspace.
41 pub title: Option<String>,
42 /// What they own: a few short phrases.
43 pub owns: Vec<String>,
44 /// When they joined the workspace (RFC 3339).
45 pub joined_at: String,
46}
47
48/// One person on a team, as the directory names them.
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct TeamPersonRef {
51 pub username: String,
52 pub role: TeamRole,
53}
54
55/// A team as the directory needs it: who is on it, by username, and who
56/// leads it.
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct DirectoryTeam {
59 pub slug: String,
60 pub name: String,
61 pub description: Option<String>,
62 pub visibility: TeamVisibility,
63 pub parent: Option<TeamRef>,
64 pub lead: Option<TeamLead>,
65 pub channel: Option<TeamChannel>,
66 /// Its own people, not its child teams'.
67 pub people: Vec<TeamPersonRef>,
68 /// Repositories it has a role on itself.
69 pub repos_count: u32,
70}
71
72/// `people_directory`: everyone in a workspace and its teams, as the
73/// viewer may see them (secret teams only for their people and owners).
74/// Members only. Returns `Outcome<PeopleDirectory>`.
75#[derive(Debug, Serialize, Deserialize)]
76pub struct PeopleArgs {
77 pub viewer: crate::Viewer,
78 pub workspace: String,
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
82pub struct PeopleDirectory {
83 pub people: Vec<DirectoryPerson>,
84 pub teams: Vec<DirectoryTeam>,
85 /// What members get on every repository: `none`, `read`, `write` or `admin`.
86 pub base_permission: String,
87 /// Whether the viewer owns the workspace: they can set anyone's
88 /// title and what they own.
89 pub can_manage: bool,
90}
91
92/// `set_member_profile`: what is given changes; the rest stays. Returns
93/// `Outcome<DirectoryPerson>`.
94#[derive(Debug, Default, Serialize, Deserialize)]
95pub struct SetMemberProfileArgs {
96 pub actor: User,
97 pub workspace: String,
98 pub username: String,
99 #[serde(default)]
100 pub title: Option<String>,
101 #[serde(default)]
102 pub owns: Option<Vec<String>>,
103 #[serde(default)]
104 pub surface: Option<crate::audit::Surface>,
105}
106
107/// A title as stored: trimmed, at most [`MAX_TITLE_LENGTH`] characters,
108/// `None` when nothing is left.
109pub fn clean_title(title: &str) -> Option<String> {
110 let title: String = title.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(MAX_TITLE_LENGTH).collect();
111 let title = title.trim().to_owned();
112 (!title.is_empty()).then_some(title)
113}
114
115/// What someone owns, as stored: each phrase trimmed and cut to
116/// [`MAX_OWNS_LENGTH`], blanks and repeats (ignoring case) left out, at
117/// most [`MAX_OWNS`].
118pub fn clean_owns(owns: &[String]) -> Vec<String> {
119 let mut out: Vec<String> = Vec::new();
120 for phrase in owns {
121 let phrase: String = phrase.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(MAX_OWNS_LENGTH).collect();
122 let phrase = phrase.trim().trim_end_matches(['.', ',', ';']).trim().to_owned();
123 if phrase.is_empty() || out.iter().any(|kept| kept.eq_ignore_ascii_case(&phrase)) {
124 continue;
125 }
126 out.push(phrase);
127 if out.len() == MAX_OWNS {
128 break;
129 }
130 }
131 out
132}
133
134/// What may change on someone's profile: their title and what they own,
135/// by themselves or an owner.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum ProfileField {
138 Title,
139 Owns,
140}
141
142/// Whether `actor` may change `field` on someone's profile.
143pub fn may_edit_profile(field: ProfileField, is_self: bool, owner: bool) -> bool {
144 match field {
145 ProfileField::Title | ProfileField::Owns => is_self || owner,
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn titles_are_tidied() {
155 assert_eq!(clean_title(" Staff Engineer "), Some("Staff Engineer".to_owned()));
156 assert_eq!(clean_title(" "), None);
157 assert_eq!(clean_title(&"a".repeat(200)).map(|t| t.len()), Some(MAX_TITLE_LENGTH));
158 }
159
160 #[test]
161 fn what_someone_owns_is_kept_short_and_once() {
162 let owns = clean_owns(&[
163 " storefront ".into(),
164 "Storefront".into(),
165 "".into(),
166 "the release process.".into(),
167 "x".repeat(100),
168 ]);
169 assert_eq!(owns[0], "storefront");
170 assert_eq!(owns[1], "the release process");
171 assert_eq!(owns[2].len(), MAX_OWNS_LENGTH);
172 assert_eq!(owns.len(), 3);
173 let many: Vec<String> = (0..20).map(|i| format!("thing {i}")).collect();
174 assert_eq!(clean_owns(&many).len(), MAX_OWNS);
175 }
176
177 #[test]
178 fn people_write_their_own_and_owners_anyones() {
179 assert!(may_edit_profile(ProfileField::Title, true, false));
180 assert!(may_edit_profile(ProfileField::Owns, false, true));
181 assert!(!may_edit_profile(ProfileField::Owns, false, false));
182 }
183}