arche / internal/archesrv/handlers_stack.go

commit 154431fd
  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}