1package archesrv
2
3import (
4 "encoding/hex"
5 "fmt"
6 "net/http"
7 "sort"
8 "strings"
9 "time"
10
11 "arche/internal/diff"
12 "arche/internal/object"
13 "arche/internal/store"
14)
15
16type stackEntry struct {
17 ChangeID string
18 HexID string
19 ShortHex string
20 Author string
21 Date string
22 Phase string
23 PhaseClass string
24 Message string
25 DiffStats string
26 SigStatus string
27 Review string
28}
29
30type srvStackData struct {
31 Repo string
32 User *User
33 StackID string
34 Entries []stackEntry
35}
36
37func (s *forgeServer) handleRepoStacks(w http.ResponseWriter, r *http.Request) {
38 repoObj, rec, ok := s.requireRepoAccess(w, r)
39 if !ok {
40 return
41 }
42 defer repoObj.Close()
43
44 bms, err := repoObj.Store.ListBookmarks()
45 if err != nil {
46 http.Error(w, err.Error(), http.StatusInternalServerError)
47 return
48 }
49
50 type stackBM struct {
51 bm store.Bookmark
52 c *object.Commit
53 }
54 var stackBMs []stackBM
55 for _, bm := range bms {
56 if !strings.HasPrefix(bm.Name, "stack/") {
57 continue
58 }
59 c, err := repoObj.ReadCommit(bm.CommitID)
60 if err != nil {
61 continue
62 }
63 stackBMs = append(stackBMs, stackBM{bm, c})
64 }
65
66 commitToStackBM := make(map[[32]byte]stackBM)
67 for _, sb := range stackBMs {
68 commitToStackBM[sb.bm.CommitID] = sb
69 }
70
71 type stackGroup struct {
72 root stackBM
73 chain []stackBM
74 }
75
76 var groups []stackGroup
77 visited := make(map[[32]byte]bool)
78
79 for _, sb := range stackBMs {
80 if visited[sb.bm.CommitID] {
81 continue
82 }
83 cur := sb
84 for {
85 if len(cur.c.Parents) == 0 {
86 break
87 }
88 parent := cur.c.Parents[0]
89 if psb, ok := commitToStackBM[parent]; ok {
90 cur = psb
91 } else {
92 break
93 }
94 }
95 if visited[cur.bm.CommitID] {
96 continue
97 }
98 var chain []stackBM
99 walk := cur
100 for {
101 visited[walk.bm.CommitID] = true
102 chain = append(chain, walk)
103 found := false
104 for _, sb2 := range stackBMs {
105 if len(sb2.c.Parents) > 0 && sb2.c.Parents[0] == walk.bm.CommitID && !visited[sb2.bm.CommitID] {
106 walk = sb2
107 found = true
108 break
109 }
110 }
111 if !found {
112 break
113 }
114 }
115 groups = append(groups, stackGroup{root: cur, chain: chain})
116 }
117
118 reviews, _ := s.db.ListStackReviews(rec.ID)
119
120 type srvStackSummary struct {
121 StackID string
122 Root string
123 Depth int
124 Reviews map[string]string
125 }
126 var summaries []srvStackSummary
127 for _, g := range groups {
128 root := "ch:" + strings.TrimPrefix(g.root.bm.Name, "stack/ch-")
129 stackID := strings.TrimPrefix(g.root.bm.Name, "stack/")
130 smap := make(map[string]string, len(g.chain))
131 for _, e := range g.chain {
132 cid := e.c.ChangeID
133 if st, ok := reviews[cid]; ok {
134 smap[cid] = st
135 } else {
136 smap[cid] = "open"
137 }
138 }
139 summaries = append(summaries, srvStackSummary{
140 StackID: stackID,
141 Root: root,
142 Depth: len(g.chain),
143 Reviews: smap,
144 })
145 }
146
147 type listData struct {
148 Repo string
149 User *User
150 Stacks []srvStackSummary
151 }
152 sort.Slice(summaries, func(i, j int) bool { return summaries[i].StackID < summaries[j].StackID })
153 s.render(w, "srv_repo_stacks.html", listData{
154 Repo: rec.Name,
155 User: s.db.currentUser(r),
156 Stacks: summaries,
157 })
158}
159
160func (s *forgeServer) handleRepoStackDetail(w http.ResponseWriter, r *http.Request) {
161 repoObj, rec, ok := s.requireRepoAccess(w, r)
162 if !ok {
163 return
164 }
165 defer repoObj.Close()
166
167 stackID := r.PathValue("stackid")
168 bmName := "stack/" + stackID
169
170 startBM, err := repoObj.Store.GetBookmark(bmName)
171 if err != nil || startBM == nil {
172 http.NotFound(w, r)
173 return
174 }
175
176 bms, _ := repoObj.Store.ListBookmarks()
177 commitToStackBM := make(map[[32]byte]store.Bookmark)
178 for _, bm := range bms {
179 if strings.HasPrefix(bm.Name, "stack/") {
180 commitToStackBM[bm.CommitID] = bm
181 }
182 }
183
184 type walkEntry struct {
185 bm store.Bookmark
186 c *object.Commit
187 }
188 var chain []walkEntry
189
190 cur := *startBM
191 curC, err := repoObj.ReadCommit(cur.CommitID)
192 if err != nil {
193 http.Error(w, "read commit: "+err.Error(), http.StatusInternalServerError)
194 return
195 }
196
197 var upChain []walkEntry
198 for {
199 upChain = append(upChain, walkEntry{cur, curC})
200 if len(curC.Parents) == 0 {
201 break
202 }
203 if pbm, ok := commitToStackBM[curC.Parents[0]]; ok {
204 pc, err := repoObj.ReadCommit(pbm.CommitID)
205 if err != nil {
206 break
207 }
208 cur = pbm
209 curC = pc
210 } else {
211 break
212 }
213 }
214 for i, j := 0, len(upChain)-1; i < j; i, j = i+1, j-1 {
215 upChain[i], upChain[j] = upChain[j], upChain[i]
216 }
217 chain = append(chain, upChain...)
218
219 visited := make(map[[32]byte]bool)
220 for _, e := range chain {
221 visited[e.bm.CommitID] = true
222 }
223 walk := stackID
224
225 _ = walk
226 for {
227 last := chain[len(chain)-1]
228 found := false
229 for _, bm := range bms {
230 if !strings.HasPrefix(bm.Name, "stack/") || visited[bm.CommitID] {
231 continue
232 }
233 c, err := repoObj.ReadCommit(bm.CommitID)
234 if err != nil {
235 continue
236 }
237 if len(c.Parents) > 0 && c.Parents[0] == last.bm.CommitID {
238 chain = append(chain, walkEntry{bm, c})
239 visited[bm.CommitID] = true
240 found = true
241 break
242 }
243 }
244 if !found {
245 break
246 }
247 }
248
249 reviews, _ := s.db.ListStackReviews(rec.ID)
250 bkMap := bookmarkMap(repoObj)
251
252 var entries []stackEntry
253 for _, e := range chain {
254 commitHex := hex.EncodeToString(e.bm.CommitID[:])
255
256 var diffStats string
257 if len(e.c.Parents) > 0 {
258 parent, err2 := repoObj.ReadCommit(e.c.Parents[0])
259 if err2 == nil {
260 fdiffs, err3 := diff.TreeDiff(repoObj, parent.TreeID, e.c.TreeID)
261 if err3 == nil {
262 added, removed := 0, 0
263 for _, fd := range fdiffs {
264 for _, line := range strings.Split(fd.Patch, "\n") {
265 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
266 added++
267 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
268 removed++
269 }
270 }
271 }
272 diffStats = fmt.Sprintf("+%d -%d across %d file(s)", added, removed, len(fdiffs))
273 }
274 }
275 }
276
277 _ = bkMap
278
279 reviewStatus, ok := reviews[e.c.ChangeID]
280 if !ok {
281 reviewStatus = "open"
282 }
283
284 phase, _ := repoObj.Store.GetPhase(e.bm.CommitID)
285 phaseName := phase.String()
286 phaseClass := strings.ToLower(phaseName)
287
288 sigStatus := s.db.GetCommitSigStatus(e.bm.CommitID)
289
290 entries = append(entries, stackEntry{
291 ChangeID: object.FormatChangeID(e.c.ChangeID),
292 HexID: commitHex,
293 ShortHex: commitHex[:8],
294 Author: e.c.Author.Name,
295 Date: e.c.Author.Timestamp.UTC().Format(time.DateTime),
296 Phase: phaseName,
297 PhaseClass: phaseClass,
298 Message: e.c.Message,
299 DiffStats: diffStats,
300 SigStatus: sigStatus,
301 Review: reviewStatus,
302 })
303 }
304
305 s.render(w, "srv_repo_stack.html", srvStackData{
306 Repo: rec.Name,
307 User: s.db.currentUser(r),
308 StackID: stackID,
309 Entries: entries,
310 })
311}
312
313func (s *forgeServer) handleStackSetReview(w http.ResponseWriter, r *http.Request) {
314 repoObj, rec, ok := s.requireRepoAccess(w, r)
315 if !ok {
316 return
317 }
318 defer repoObj.Close()
319
320 user := s.db.currentUser(r)
321 if user == nil {
322 http.Error(w, "login required", http.StatusUnauthorized)
323 return
324 }
325
326 stackID := r.PathValue("stackid")
327 changeID := r.PathValue("changeid")
328
329 if err := r.ParseForm(); err != nil {
330 http.Error(w, "bad form", http.StatusBadRequest)
331 return
332 }
333 status := r.FormValue("status")
334 switch status {
335 case "open", "reviewing", "approved", "needs-revision":
336 default:
337 http.Error(w, "invalid status", http.StatusBadRequest)
338 return
339 }
340
341 if err := s.db.SetStackReview(rec.ID, changeID, status, user.ID); err != nil {
342 http.Error(w, err.Error(), http.StatusInternalServerError)
343 return
344 }
345
346 http.Redirect(w, r, "/"+rec.Name+"/stacks/"+stackID, http.StatusSeeOther)
347}