Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 2h ago 4b8c9e1d
history blame raw 3.7 KiB
1package index
2
3// One account's numbers, read from the feed's own rows.
4//
5// A profile could count issues, changes, approvals and comments from the
6// per-entity tables, but then "12 issues opened" and the activity feed would be
7// two statements about the same history that can disagree. The events table
8// already holds exactly what the feed shows — one row per opening, comment,
9// approval, state change and release — so the numbers are a grouped read of those
10// rows. An account's profile and the instance feed cannot drift apart, because
11// they are the same rows.
12//
13// Whether a row is visible is the caller's question, not this layer's: every
14// function here takes the repository ids the viewer may read.
15
16// ActorCount is one cell of a profile's numbers: how many events of one kind and
17// type an account produced in one repository. State changes keep their state, so
18// a closed issue and a reopened one are not mistaken for each other.
19type ActorCount struct {
20 RepoID string
21 Kind string // issue | change | release
22 Type string // open | comment | approval | state | release
23 State string // closed | merged | open, for state events
24 N int
25}
26
27// ActorActivity returns an account's events grouped by repository, kind, type and
28// state. It is the profile's whole read: the stat panel and the per-repository
29// cards are this one answer arranged two ways.
30func (s *Store) ActorActivity(repoIDs []string, actor string) ([]ActorCount, error) {
31 if len(repoIDs) == 0 || actor == "" {
32 return nil, nil
33 }
34 args := make([]any, 0, len(repoIDs)+1)
35 for _, id := range repoIDs {
36 args = append(args, id)
37 }
38 args = append(args, actor)
39 rows, err := s.db.Query(
40 `SELECT repo_id, kind, type, state, COUNT(*) FROM events
41 WHERE repo_id IN (`+questionMarks(len(repoIDs))+`) AND actor=?
42 GROUP BY repo_id, kind, type, state`, args...)
43 if err != nil {
44 return nil, err
45 }
46 defer rows.Close()
47 var out []ActorCount
48 for rows.Next() {
49 var c ActorCount
50 if err := rows.Scan(&c.RepoID, &c.Kind, &c.Type, &c.State, &c.N); err != nil {
51 return nil, err
52 }
53 out = append(out, c)
54 }
55 return out, rows.Err()
56}
57
58// ActorSummary is what a directory row says about one account: how many
59// repositories they touched, and the three counts worth putting beside a name.
60type ActorSummary struct {
61 Repos int
62 Issues int
63 Changes int
64 Reviews int
65}
66
67// ActorSummaries answers for every account with visible work in one grouped
68// query, rather than one query per name. It returns the whole map so a caller can
69// sort a directory by activity before it pages it: a page of names cannot be
70// sorted by a count it has not read. Accounts with nothing visible are absent,
71// which the directory renders as no activity, not as a zero somebody should be
72// embarrassed by.
73func (s *Store) ActorSummaries(repoIDs []string) (map[string]ActorSummary, error) {
74 out := map[string]ActorSummary{}
75 if len(repoIDs) == 0 {
76 return out, nil
77 }
78 args := make([]any, 0, len(repoIDs))
79 for _, id := range repoIDs {
80 args = append(args, id)
81 }
82 rows, err := s.db.Query(
83 `SELECT actor,
84 COUNT(DISTINCT repo_id),
85 COALESCE(SUM(CASE WHEN kind='issue' AND type='open' THEN 1 ELSE 0 END),0),
86 COALESCE(SUM(CASE WHEN kind='change' AND type='open' THEN 1 ELSE 0 END),0),
87 COALESCE(SUM(CASE WHEN type='approval' THEN 1 ELSE 0 END),0)
88 FROM events
89 WHERE repo_id IN (`+questionMarks(len(repoIDs))+`)
90 GROUP BY actor`, args...)
91 if err != nil {
92 return nil, err
93 }
94 defer rows.Close()
95 for rows.Next() {
96 var (
97 name string
98 sum ActorSummary
99 )
100 if err := rows.Scan(&name, &sum.Repos, &sum.Issues, &sum.Changes, &sum.Reviews); err != nil {
101 return nil, err
102 }
103 out[name] = sum
104 }
105 return out, rows.Err()
106}
107