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

Live pages of repositories a private work item only names hear nothing of it

A work item's change goes to its origin (the event's repository) and to the other repositories it is linked to only when its origin is public, decided with one anonymous readable call per batch. An item with no origin reaches no repository page, not one its data names.

G1T-Session:
ses_01m56w3nnce54skbep969amaqy
syntaqxcommitted Parent17dfc27Browse files
2 files+81−200/2 viewed
+1−1
265265 let notify = self.env.service("NOTIFY").ok();
266266 // Pages open on the repositories: told first, as nobody waits longer.
267267 if let Some(notify) = notify.as_ref() {
268− live::tell(notify, events).await;
268+ live::tell(notify, &repos, events).await;
269269 }
270270 let sources = inbox::Sources {
271271 work: &work,
+80−19
55 //! along, and who may see it is notify's to keep: a tab watches a
66 //! repository only after the site checked its person may read it.
77 //!
8−//! A work item's events and comments go to every repository it is linked
9−//! to (`repoIds`), not only its origin, which it may not have, and to its
10−//! workspace's Work pages (`workspace_changed`), which hear of it with or
11−//! without a repository.
8+//! A work item's events and comments go to its origin (the event's own
9+//! repository, which it may not have) and to its workspace's Work pages
10+//! (`workspace_changed`), which hear of it with or without a repository.
11+//! The other repositories it is linked to (`repoIds`) hear of it only when
12+//! their readers may all read it: when its origin is public. A private
13+//! one's kind and number are not for whoever reads a repository it only
14+//! names, nor is an item with no origin, which is its workspace's alone.
1215
1316 use g1t_contracts::events::Event;
1417 use g1t_contracts::live::{
1518 MAX_CHANGES, REPO_CHANGED, RepoChange, RepoChangedArgs, WORKSPACE_CHANGED, WorkspaceChangedArgs, change_of, workspace_changes_of,
1619 };
20+use g1t_contracts::repos::{MAX_READABLE, ReadableArgs, Repo};
21+use std::collections::HashSet;
1722 use worker::{Fetcher, Result};
1823
1924 /// The changes an event makes: one for its repository, and one for each
20−/// other repository it names in `repoIds`.
21−pub(crate) fn changes_for(event: &Event) -> Vec<RepoChange> {
22− let mut changes: Vec<RepoChange> = change_of(event).into_iter().collect();
25+/// other repository it names in `repoIds` when the event's repository (a
26+/// work item's origin) is among the `public` ones.
27+pub(crate) fn changes_for(event: &Event, public: &HashSet<String>) -> Vec<RepoChange> {
28+ let linked_to = event.data["repoIds"].as_array().is_some_and(|ids| !ids.is_empty());
29+ let origin = event.repo_id.as_deref().filter(|id| !id.is_empty());
30+ // A work item's origin is the event's own repository, never a
31+ // `repoId` in its data.
32+ let mut changes: Vec<RepoChange> = if linked_to && origin.is_none() {
33+ Vec::new()
34+ } else {
35+ change_of(event).into_iter().collect()
36+ };
37+ if !origin.is_some_and(|origin| public.contains(origin)) {
38+ return changes;
39+ }
2340 for repo_id in event.data["repoIds"].as_array().into_iter().flatten().filter_map(|id| id.as_str()) {
2441 if repo_id.is_empty() || changes.iter().any(|change| change.repo_id == repo_id) {
2542 continue;
3148 }
3249
3350 /// A batch's changes, in order, in calls of at most [`MAX_CHANGES`].
34−pub(crate) fn changes_of(events: &[Event]) -> Vec<Vec<RepoChange>> {
35− let changes: Vec<RepoChange> = events.iter().flat_map(changes_for).collect();
51+pub(crate) fn changes_of(events: &[Event], public: &HashSet<String>) -> Vec<Vec<RepoChange>> {
52+ let changes: Vec<RepoChange> = events.iter().flat_map(|event| changes_for(event, public)).collect();
3653 changes.chunks(MAX_CHANGES).map(<[RepoChange]>::to_vec).collect()
3754 }
3855
56+/// Which of these repositories anyone may read, in one `readable` call
57+/// with no viewer. None when the repos service cannot say: nothing is
58+/// taken to be public that is not known to be.
59+pub(crate) async fn public_of(repos: &Fetcher, ids: Vec<String>) -> HashSet<String> {
60+ if ids.is_empty() {
61+ return HashSet::new();
62+ }
63+ let readable: Result<Vec<Repo>> = g1t_kit::call(repos, "readable", &ReadableArgs { ids, viewer: None }).await;
64+ match readable {
65+ Ok(repos) => repos.into_iter().map(|repo| repo.id).collect(),
66+ Err(error) => {
67+ worker::console_error!("events: could not tell which repositories are public: {error}");
68+ HashSet::new()
69+ }
70+ }
71+}
72+
73+/// The origins of the batch's events that name other repositories
74+/// (`repoIds`): the ones whose publicness decides who else hears.
75+fn origins_linked(events: &[Event]) -> Vec<String> {
76+ let origins: HashSet<String> = events
77+ .iter()
78+ .filter(|event| event.data["repoIds"].as_array().is_some_and(|ids| ids.len() > 1))
79+ .filter_map(|event| event.repo_id.clone().filter(|id| !id.is_empty()))
80+ .collect();
81+ origins.into_iter().take(MAX_READABLE).collect()
82+}
83+
3984 /// Tells notify of the batch's changes. Logged and left when it cannot be
4085 /// reached: pages then catch up by themselves, and nothing else waits on it.
41−pub(crate) async fn tell(notify: &Fetcher, events: &[Event]) {
42− for changes in changes_of(events) {
86+pub(crate) async fn tell(notify: &Fetcher, repos: &Fetcher, events: &[Event]) {
87+ let public = public_of(repos, origins_linked(events)).await;
88+ for changes in changes_of(events, &public) {
4389 let told: Result<serde_json::Value> = g1t_kit::call(notify, REPO_CHANGED, &RepoChangedArgs { changes }).await;
4490 if let Err(error) = told {
4591 worker::console_error!("events: live pages not told: {error}");
70116 }
71117 }
72118
119+ fn public(ids: &[&str]) -> HashSet<String> {
120+ ids.iter().map(|id| (*id).to_owned()).collect()
121+ }
122+
73123 #[test]
74− fn a_work_item_changes_every_repository_it_is_linked_to() {
124+ fn a_public_work_item_changes_every_repository_it_is_linked_to() {
75125 let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "repoIds": ["rep_1", "rep_2"] });
76− let repos = |event: &Event| changes_for(event).into_iter().map(|change| (change.repo_id, change.number)).collect::<Vec<_>>();
77− let with_origin = event("work_item.updated", Some("rep_1"), data.clone());
78− assert_eq!(repos(&with_origin), vec![("rep_1".to_owned(), Some(40)), ("rep_2".to_owned(), Some(40))]);
79− // Filed in the workspace: only the repositories it is linked to.
80− let without = event("work_item.linked", None, data);
81− assert_eq!(repos(&without).len(), 2);
126+ let with_origin = event("work_item.updated", Some("rep_1"), data);
127+ let repos: Vec<_> = changes_for(&with_origin, &public(&["rep_1"])).into_iter().map(|change| (change.repo_id, change.number)).collect();
128+ assert_eq!(repos, vec![("rep_1".to_owned(), Some(40)), ("rep_2".to_owned(), Some(40))]);
129+ assert_eq!(origins_linked(&[with_origin]), vec!["rep_1".to_owned()]);
82130 // What changes no page stays quiet.
83− assert!(changes_for(&event("audit.recorded", None, json!({ "repoIds": ["rep_1"] }))).is_empty());
131+ assert!(changes_for(&event("audit.recorded", None, json!({ "repoIds": ["rep_1"] })), &public(&["rep_1"])).is_empty());
132+ }
133+
134+ #[test]
135+ fn a_private_work_item_changes_only_its_origin() {
136+ let repos = |event: &Event| changes_for(event, &public(&["rep_9"])).into_iter().map(|change| change.repo_id).collect::<Vec<_>>();
137+ // Its origin is private: the repositories it only names hear nothing.
138+ let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "repoIds": ["rep_1", "rep_2"] });
139+ assert_eq!(repos(&event("work_item.updated", Some("rep_1"), data)), vec!["rep_1".to_owned()]);
140+ // Filed in the workspace: no repository hears of it, not even the
141+ // first it is linked to, nor one its data names.
142+ let filed = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "repoId": "rep_2", "repoIds": ["rep_2", "rep_9"] });
143+ assert!(repos(&event("work_item.linked", None, filed.clone())).is_empty());
144+ assert!(repos(&event("comment.created", None, filed)).is_empty());
84145 }
85146 }

Comments

Sign in to comment.