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/detail.go

Line attribution on main · 174 line(s)

9d8dd1bc
Keenan Nemetz
10d ago
1 package index
2
3 // One issue or change request, assembled from the derived rows for rendering.
4 //
5 // The detail pages used to read their record from git so a body could never be
6 // stale. They can read the index for the same reason the list pages do: the
7 // handler refreshes the repository at the start of the request (Ensure), so the
8 // rows describe the branch as of that commit. A write request does not use this
9 // path — it may have just changed the record — and neither does a server without
10 // an index; both keep reading git through metadata.
11
12 import (
13 "database/sql"
14 "errors"
15 "strings"
16
d37d301f
Keenan Nemetz
22h ago
17 "rodent.software/vole/metadata"
9d8dd1bc
Keenan Nemetz
10d ago
18 )
19
20 // Issue returns one issue with its comments. The comments are in creation order,
21 // which is the order the thread renders.
22 func (s *Store) Issue(repoID, id string) (*metadata.Issue, error) {
23 var (
24 iss metadata.Issue
25 created int64
26 labels string
27 assignees string
28 )
29 err := s.db.QueryRow(
30 `SELECT id, title, state, author, created, labels, assignees, body
31 FROM issues WHERE repo_id=? AND id=?`, repoID, id).
32 Scan(&iss.ID, &iss.Title, &iss.State, &iss.Author, &created, &labels, &assignees, &iss.Description)
33 if errors.Is(err, sql.ErrNoRows) {
34 return nil, ErrNotFound
35 }
36 if err != nil {
37 return nil, err
38 }
39 iss.Created = unixTime(created)
40 iss.Labels = splitList(labels)
41 iss.Assignees = splitList(assignees)
42 iss.Path = metadata.IssuesDir + "/" + id
43 comments, err := s.Comments(repoID, commentKindIssue, id)
44 if err != nil {
45 return nil, err
46 }
47 iss.Comments = commentList(comments)
48 return &iss, nil
49 }
50
51 // Request returns one change request with its closes, approvals and comments.
52 func (s *Store) Request(repoID, id string) (*metadata.Request, error) {
53 var (
54 req metadata.Request
55 created int64
56 mergedAt int64
57 )
58 err := s.db.QueryRow(
59 `SELECT id, title, state, author, created, base, head, head_ref,
60 merged_base, merged_head, merged_by, merged_at, body
61 FROM requests WHERE repo_id=? AND id=?`, repoID, id).
62 Scan(&req.ID, &req.Title, &req.State, &req.Author, &created, &req.Base, &req.Head,
63 &req.HeadRef, &req.MergedBase, &req.MergedHead, &req.MergedBy, &mergedAt, &req.Description)
64 if errors.Is(err, sql.ErrNoRows) {
65 return nil, ErrNotFound
66 }
67 if err != nil {
68 return nil, err
69 }
70 req.Created = unixTime(created)
71 req.MergedAt = unixTime(mergedAt)
72
73 rows, err := s.db.Query(
74 `SELECT issue_id FROM request_closes WHERE repo_id=? AND request_id=? ORDER BY position, issue_id`,
75 repoID, id)
76 if err != nil {
77 return nil, err
78 }
79 for rows.Next() {
80 var issueID string
81 if err := rows.Scan(&issueID); err != nil {
82 rows.Close()
83 return nil, err
84 }
85 req.Closes = append(req.Closes, issueID)
86 }
87 if err := rows.Err(); err != nil {
88 rows.Close()
89 return nil, err
90 }
91 rows.Close()
92
93 rows, err = s.db.Query(
94 `SELECT principal, head, base, created FROM request_approvals
95 WHERE repo_id=? AND request_id=? ORDER BY principal`, repoID, id)
96 if err != nil {
97 return nil, err
98 }
99 for rows.Next() {
100 var (
101 a metadata.Approval
102 created int64
103 )
104 if err := rows.Scan(&a.Principal, &a.Head, &a.Base, &created); err != nil {
105 rows.Close()
106 return nil, err
107 }
108 a.Created = unixTime(created)
109 req.Approvals = append(req.Approvals, a)
110 }
111 if err := rows.Err(); err != nil {
112 rows.Close()
113 return nil, err
114 }
115 rows.Close()
116
117 comments, err := s.Comments(repoID, commentKindRequest, id)
118 if err != nil {
119 return nil, err
120 }
121 req.Comments = commentList(comments)
122 return &req, nil
123 }
124
125 // AllApprovals returns every approval in a repository, keyed by request id. The
126 // activity feed needs all of them; a list page asks for the rows it shows instead.
127 func (s *Store) AllApprovals(repoID string) (map[string][]ApprovalRow, error) {
128 rows, err := s.db.Query(
129 `SELECT request_id, principal, head, base, created FROM request_approvals
130 WHERE repo_id=? ORDER BY request_id, principal`, repoID)
131 if err != nil {
132 return nil, err
133 }
134 defer rows.Close()
135 out := map[string][]ApprovalRow{}
136 for rows.Next() {
137 var id string
138 var a ApprovalRow
139 var created int64
140 if err := rows.Scan(&id, &a.Principal, &a.Head, &a.Base, &created); err != nil {
141 return nil, err
142 }
143 a.Created = unixTime(created)
144 out[id] = append(out[id], a)
145 }
146 return out, rows.Err()
147 }
148
149 // splitList decodes the \x1f-separated label and assignee columns.
150 func splitList(s string) []string {
151 if s == "" {
152 return nil
153 }
154 return strings.Split(s, "\x1f")
155 }
156
157 // commentList turns indexed rows back into the comment shape a page renders.
158 func commentList(rows []CommentRow) []metadata.Comment {
159 if len(rows) == 0 {
160 return nil
161 }
162 out := make([]metadata.Comment, 0, len(rows))
163 for _, c := range rows {
164 comment := metadata.Comment{
165 ID: c.ID, Author: c.Author, Created: c.Created, Body: c.Body,
166 ReplyTo: c.ReplyTo, Deleted: c.Deleted,
167 }
168 if c.Path != "" {
169 comment.Anchor = &metadata.Anchor{Path: c.Path, Side: c.Side, Line: c.Line, Blob: c.Blob}
170 }
171 out = append(out, comment)
172 }
173 return out
174 }