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.

Commit

The inbox files a work item under its origin alone, and tells who only watches a linked repository only when they may read it

An item event's repository is the envelope's (its origin, or none): never a repoId in its data. Someone who hears of an item only by watching another repository it is linked to gets no row, push, email or count unless they are a member of its workspace or its origin is public.

G1T-Session:
ses_01m56w3nnce54skbep969amaqy
syntaqxcommitted Parent5faf042Browse files
1 file+92−400/1 viewed
+92−40
4242 //! hear of it; whoever ignores its origin hears of nothing on it. Its
4343 //! items are kept with its workspace and its origin, if it has one, and
4444 //! are shown to the workspace's members and to whoever can read the
45−//! origin. Those who hear of it only by watching another repository it is
46−//! linked to are not pushed it live: whether they may see it is known only
47−//! when their inbox is read.
45+//! origin (the event's own repository, never the first of `repoIds`).
46+//! Those who hear of it only by watching another repository it is linked
47+//! to are told only when they may read it: members of its workspace, or
48+//! anyone when its origin is public.
4849 //!
4950 //! Nobody is told of what they did themselves, though an outcome they set
5051 //! off (checks, a workflow, a deployment, g1t's work) is theirs to hear of.
172173 let data = &event.data;
173174 let text = |key: &str| data[key].as_str().filter(|value| !value.is_empty()).map(str::to_owned);
174175 let number = |key: &str| data[key].as_u64().and_then(|n| u32::try_from(n).ok());
175− let repo_id = text("repoId").or_else(|| event.repo_id.clone());
176176 // A work item's events, and comments on one: it may have no origin.
177+ // Its origin is the event's own repository and nothing else: never a
178+ // `repoId` in its data, nor the first of `repoIds`, which may be a
179+ // repository it is only linked to, whose readers may not see it.
177180 let on_item = event.kind == "comment.created" && data["itemId"].is_string();
178181 if ITEM_TOLD.contains(&event.kind.as_str()) || on_item {
179182 return Some(Wanted {
180− repo_id,
183+ repo_id: event.repo_id.clone().filter(|id| !id.is_empty()),
181184 number: Some(number("number")?),
182185 comment_id: if on_item { Some(text("commentId")?) } else { None },
183186 item: Some(item_of(data)?),
184187 });
185188 }
186− let repo_id = repo_id?;
189+ let repo_id = text("repoId").or_else(|| event.repo_id.clone())?;
187190 let on = |number: Option<u32>, comment_id: Option<String>| {
188191 Some(Wanted {
189192 repo_id: Some(repo_id.clone()),
13921395 event_id: String,
13931396 /// Its event leaves nothing to do (`CLOSES`): settled once written.
13941397 closing: bool,
1395− /// Pushed live: not for someone who hears of a work item only by
1396− /// watching a repository it is linked to other than its origin, unless
1397− /// they are a member of its workspace, who may see it.
1398− live: bool,
13991398 /// The thread it is on, which notify keeps with what it replays.
14001399 thread: String,
14011400 }
15071506 if let Some(notify) = sources.notify {
15081507 let live = written
15091508 .iter()
1510− .filter(|item| !item.closing && item.live)
1509+ .filter(|item| !item.closing)
15111510 .map(|item| {
15121511 let live = live_notification(&item.event_id, &item.notice, &item.url, &rfc3339(now_ms()), Some(&item.thread));
15131512 (item.notice.username.clone(), live)
17411740 None => repo.clone().unwrap_or_default(),
17421741 };
17431742 let mut told = notices(event, &place, &actor, subject.as_ref(), &audience);
1743+ // Who hears of a work item only by watching another repository it is
1744+ // linked to is told only if they may read it: a member of its
1745+ // workspace, or anyone when its origin is public. Nothing of it (title,
1746+ // body, push, email, count) reaches anyone else.
1747+ if let Some(item) = &wanted.item {
1748+ let hearsay: Vec<String> = told
1749+ .iter()
1750+ .filter(|notice| only_elsewhere(notice, &elsewhere))
1751+ .map(|notice| notice.username.clone())
1752+ .collect();
1753+ if !hearsay.is_empty() {
1754+ let asked = MembersAmongArgs { workspace: item.workspace.clone(), usernames: hearsay.clone() };
1755+ let members: HashSet<String> = g1t_kit::call::<_, Vec<String>>(sources.identity, "members_among", &asked)
1756+ .await
1757+ .unwrap_or_else(|error| {
1758+ worker::console_error!("inbox: could not tell who is a member of {}: {error}", item.workspace);
1759+ Vec::new()
1760+ })
1761+ .into_iter()
1762+ .map(|name| name.to_lowercase())
1763+ .collect();
1764+ let public = match &wanted.repo_id {
1765+ Some(origin) if hearsay.iter().any(|name| !members.contains(name)) => {
1766+ crate::live::public_of(sources.repos, vec![origin.clone()]).await.contains(origin)
1767+ }
1768+ _ => false,
1769+ };
1770+ told.retain(|notice| may_hear(notice, &elsewhere, &members, public));
1771+ }
1772+ }
17441773 let mut statements =
17451774 subscriptions::auto_subscribe(db, &thread.key, wanted.repo_id.as_deref(), &subscribes(event, subject.as_ref()), &event.time)?;
17461775 // A scan's summary: one that opened nothing brings the thread up to date
18481877 news.extend(rows.into_iter().map(|row| row.username));
18491878 }
18501879 }
1851− // Who hears of a work item only by watching another of its
1852− // repositories is pushed it live when they are a member of its
1853− // workspace, who may see it (all one filed with no repository has).
1854− let members: HashSet<String> = match &wanted.item {
1855− Some(item) => {
1856− let usernames: Vec<String> = told
1857− .iter()
1858− .filter(|notice| notice.reason == Reason::Subscribed && elsewhere.contains(&notice.username) && news.contains(&notice.username))
1859− .map(|notice| notice.username.clone())
1860− .collect();
1861− if usernames.is_empty() {
1862− HashSet::new()
1863− } else {
1864− let asked = MembersAmongArgs { workspace: item.workspace.clone(), usernames };
1865− g1t_kit::call::<_, Vec<String>>(sources.identity, "members_among", &asked)
1866− .await
1867− .unwrap_or_else(|error| {
1868− worker::console_error!("inbox: could not tell who is a member of {}: {error}", item.workspace);
1869− Vec::new()
1870− })
1871− .into_iter()
1872− .collect()
1873− }
1874− }
1875− None => HashSet::new(),
1876− };
18771880 Ok(told
18781881 .into_iter()
18791882 .filter(|notice| news.contains(&notice.username))
18801883 .map(|notice| Written {
1881− live: !(notice.reason == Reason::Subscribed
1882− && elsewhere.contains(&notice.username)
1883− && !members.contains(&notice.username)),
18841884 notice,
18851885 repo_id: wanted.repo_id.clone(),
18861886 workspace: wanted.item.as_ref().map(|item| item.workspace.clone()),
18921892 .collect())
18931893 }
18941894
1895+/// Whether a notice is someone's only by watching a repository a work item
1896+/// is linked to other than its origin: whether they may read it is not
1897+/// known from that.
1898+fn only_elsewhere(notice: &Notice, elsewhere: &HashSet<String>) -> bool {
1899+ notice.reason == Reason::Subscribed && elsewhere.contains(&notice.username)
1900+}
1901+
1902+/// Whether a work item's notice may be written: anyone told for a reason
1903+/// of their own, or who watches its origin; one who only watches another
1904+/// repository it is linked to (`elsewhere`) when they may read it, as a
1905+/// member of its workspace (`members`) or because its origin is public.
1906+/// An item with no origin is its workspace members' alone.
1907+fn may_hear(notice: &Notice, elsewhere: &HashSet<String>, members: &HashSet<String>, origin_public: bool) -> bool {
1908+ !only_elsewhere(notice, elsewhere) || origin_public || members.contains(&notice.username)
1909+}
1910+
18951911 /// Brings a person's thread up with new activity, or starts it. `?1` id,
18961912 /// `?2` username, `?3` thread, `?4` event id, `?5` event type, `?6` reason,
18971913 /// `?7` severity, `?8` title, `?9` body, `?10` workspace, `?11` repo id,
28312847 }
28322848
28332849 #[test]
2850+ fn a_work_items_origin_is_its_events_repository_alone() {
2851+ // Filed in the workspace and linked to rep_2: no origin, whatever
2852+ // its data's `repoId` or the first of its `repoIds` say.
2853+ let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "commentId": "cmt_1", "repoId": "rep_2", "repoIds": ["rep_2"] });
2854+ for kind in ["work_item.opened", "comment.created"] {
2855+ let wanted = wants(&Event { repo_id: None, ..event(kind, None, data.clone()) }).unwrap_or_else(|| panic!("{kind}"));
2856+ assert_eq!(wanted.repo_id, None, "{kind}");
2857+ }
2858+ // With an origin, the event's repository, not its data's.
2859+ let wanted = wants(&event("comment.created", None, data)).unwrap();
2860+ assert_eq!(wanted.repo_id.as_deref(), Some("rep_1"));
2861+ }
2862+
2863+ #[test]
2864+ fn who_only_watches_a_linked_repository_hears_of_a_work_item_they_may_read() {
2865+ let notice = |username: &str, reason: Reason| Notice {
2866+ username: username.into(),
2867+ reason,
2868+ severity: Severity::Info,
2869+ title: "acme#40 Secret plans".into(),
2870+ body: String::new(),
2871+ };
2872+ let set = |names: &[&str]| names.iter().map(|name| (*name).to_owned()).collect::<HashSet<String>>();
2873+ let elsewhere = set(&["cy", "dee"]);
2874+ let members = set(&["dee"]);
2875+ // Its origin's watchers and whoever is told for a reason of their own.
2876+ assert!(may_hear(&notice("ana", Reason::Subscribed), &elsewhere, &members, false));
2877+ assert!(may_hear(&notice("cy", Reason::Mention), &elsewhere, &members, false));
2878+ // A member of its workspace may read it.
2879+ assert!(may_hear(&notice("dee", Reason::Subscribed), &elsewhere, &members, false));
2880+ // Anyone else only when its origin is public.
2881+ assert!(!may_hear(&notice("cy", Reason::Subscribed), &elsewhere, &members, false));
2882+ assert!(may_hear(&notice("cy", Reason::Subscribed), &elsewhere, &members, true));
2883+ }
2884+
2885+ #[test]
28342886 fn a_work_items_watchers_are_its_origins_then_its_other_repositories() {
28352887 let watcher = |name: &str, level: WatchLevel| Watcher { username: name.into(), level, events: Vec::new() };
28362888 let own = vec![watcher("ana", WatchLevel::All), watcher("bo", WatchLevel::Ignore)];

Comments

Sign in to comment.