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/web.go +19−18
| @@ -67,24 +67,25 @@ func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Sto | |||
| 67 | 67 | // below can be a method: metadata files carry account names and approvals are | |
| 68 | 68 | // keyed by ids, and a page should show the display label for either. | |
| 69 | 69 | w.Tmpl, err = template.New("vole").Funcs(template.FuncMap{ | |
| 70 | "since": humanSince, | ||
| 71 | "short": format.SHA, | ||
| 72 | "esc": url.PathEscape, | ||
| 73 | "bytes": format.Bytes, | ||
| 74 | "credid": identity.CredentialIDString, | ||
| 75 | "who": w.who, | ||
| 76 | "nav": w.repoNav, | ||
| 77 | "inc": func(i int) int { return i + 1 }, | ||
| 78 | "initial": initial, | ||
| 79 | "tone": tone, | ||
| 80 | "urlquery": url.QueryEscape, | ||
| 81 | "join": strings.Join, | ||
| 82 | "tokenPicker": tokenPicker, | ||
| 83 | "icon": icon, | ||
| 84 | "treeContext": treeContext, | ||
| 85 | "pathurl": pathURL, | ||
| 86 | "pathjoin": joinPath, | ||
| 87 | "parentpath": parentPath, | ||
| 70 | "since": humanSince, | ||
| 71 | "short": format.SHA, | ||
| 72 | "esc": url.PathEscape, | ||
| 73 | "bytes": format.Bytes, | ||
| 74 | "credid": identity.CredentialIDString, | ||
| 75 | "who": w.who, | ||
| 76 | "nav": w.repoNav, | ||
| 77 | "inc": func(i int) int { return i + 1 }, | ||
| 78 | "initial": initial, | ||
| 79 | "tone": tone, | ||
| 80 | "urlquery": url.QueryEscape, | ||
| 81 | "join": strings.Join, | ||
| 82 | "tokenPicker": tokenPicker, | ||
| 83 | "icon": icon, | ||
| 84 | "treeContext": treeContext, | ||
| 85 | "reviewTreeContext": reviewTreeContext, | ||
| 86 | "pathurl": pathURL, | ||
| 87 | "pathjoin": joinPath, | ||
| 88 | "parentpath": parentPath, | ||
| 88 | 89 | }).ParseFS(templateFS, "templates/*.html") | |
| 89 | 90 | if err != nil { | |
| 90 | 91 | return nil, err | |