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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Blame · forge/index/profile.go

Line attribution on main · 106 line(s)

a6b42433
Keenan Nemetz
8d ago
1 package 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.
19 type 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.
30 func (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.
60 type ActorSummary struct {
61 Repos int
62 Issues int
63 Changes int
64 Reviews int
65 }
66
4739664b
Keenan Nemetz
8d ago
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.
73 func (s *Store) ActorSummaries(repoIDs []string) (map[string]ActorSummary, error) {
a6b42433
Keenan Nemetz
8d ago
74 out := map[string]ActorSummary{}
4739664b
Keenan Nemetz
8d ago
75 if len(repoIDs) == 0 {
a6b42433
Keenan Nemetz
8d ago
76 return out, nil
77 }
4739664b
Keenan Nemetz
8d ago
78 args := make([]any, 0, len(repoIDs))
a6b42433
Keenan Nemetz
8d ago
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 }