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.

Build out change review and threaded discussions

f6f4b1db24292d1df877680714952a9c568e2b78 · Keenan Nemetz · 15d ago · parent 564acd4b

Build out change review and threaded discussions

- Add Conversation, Files and Runs tabs to change requests. Files uses the
  code-browser shape with a changed-file tree and one selected-file diff;
  Runs lists the head commit's workflow results.
- Show anchored review comments beneath the line, with a collapsed excerpt
  read from the exact blob that was reviewed, and return posted anchors to
  their line note.
- Add comment threads: any comment can be replied to, the UI flattens the
  descendants under one root, reply context links back to the parent, and
  reply targets feed a single composer at the foot of the discussion.
- Collapse reply groups while keeping the root visible, and scroll to the
  composer when Reply is selected.
- Add comment context menus with Reply, Copy text, Copy link, Edit and
  Delete. Add web edit/delete routes with author or admin checks; deletion
  leaves a tombstone so replies keep their thread.
- Support --reply-to in the issue and change CLI commands, and treat a
  missing parent as a tombstone rather than an invalid tree.
- Fix the diff comment gutter, action button colours, reply labels, and
  keep the Files tree and diff pane aligned.

Update metadata validation, web templates, CSS, JavaScript, tests, and the
CLI/change-request/issue docs.

server/web/request_files_test.go +52

@@ -0,0 +1,52 @@
1 package web
2
3 import "testing"
4
5 func findTreeNode(nodes []*treeNode, path string) *treeNode {
6 for _, n := range nodes {
7 if n.Path == path {
8 return n
9 }
10 if n.Type == "tree" {
11 if found := findTreeNode(n.Children, path); found != nil {
12 return found
13 }
14 }
15 }
16 return nil
17 }
18
19 func TestBuildReviewTreeStatusesAndActiveAncestors(t *testing.T) {
20 files := []diffFile{
21 {NewPath: "docs/new.md"},
22 {OldPath: "old.go", NewPath: "new.go"},
23 {OldPath: "removed.txt"},
24 {OldPath: "keep.go", NewPath: "keep.go"},
25 }
26 tree := buildReviewTree(files, "docs/new.md")
27
28 for path, want := range map[string]string{
29 "docs/new.md": "A",
30 "new.go": "R",
31 "removed.txt": "D",
32 "keep.go": "M",
33 } {
34 node := findTreeNode(tree, path)
35 if node == nil {
36 t.Fatalf("tree is missing %s", path)
37 }
38 if node.Status != want {
39 t.Fatalf("%s status = %q, want %q", path, node.Status, want)
40 }
41 }
42 if node := findTreeNode(tree, "docs/new.md"); !node.Active {
43 t.Fatal("the selected file is not active")
44 }
45 if docs := findTreeNode(tree, "docs"); docs == nil || !docs.Open {
46 t.Fatal("the selected file's directory is not open")
47 }
48 // Deleted files keep their old path; renamed files take the new one.
49 if findTreeNode(tree, "old.go") != nil {
50 t.Fatal("the renamed file is still keyed by its old path")
51 }
52 }