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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0: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.

metadata/request_write.go +22−4

@@ -240,9 +240,13 @@ func AddRequestComment(g *git.Repo, requestID string, c *Comment, actor string)
240 240 }
241 241 path := RequestCommentPath(requestID, c.ID)
242 242 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
243 if _, err := loadFrom(files, requestID); err != nil {
243 r, err := loadFrom(files, requestID)
244 if err != nil {
244 245 return err
245 246 }
247 if prob := checkCommentReply(r.Comments, c); prob != "" {
248 return fmt.Errorf("requests: %s", prob)
249 }
246 250 if _, exists := files[path]; exists {
247 251 return fmt.Errorf("requests: comment %s already exists", c.ID)
248 252 }
@@ -260,6 +264,9 @@ func EditRequestComment(g *git.Repo, requestID, commentID, body, actor string) (
260 264 if err != nil {
261 265 return nil, err
262 266 }
267 if c.Deleted {
268 return nil, fmt.Errorf("requests: cannot edit a deleted comment")
269 }
263 270 c.Body = strings.TrimRight(body, "\n")
264 271 if err := validateComment(c); err != nil {
265 272 return nil, err
@@ -281,18 +288,29 @@ func EditRequestComment(g *git.Repo, requestID, commentID, body, actor string) (
281 288 return c, nil
282 289 }
283 290
284 // DeleteComment removes a comment's file. The text stays in history: deleting is
285 // not redaction, which is an admin force-push.
291 // DeleteComment replaces a comment with a tombstone. The text stays in history:
292 // deleting is not redaction, which is an admin force-push. Replies keep the
293 // parent they answer.
286 294 func DeleteRequestComment(g *git.Repo, requestID, commentID, actor string) (*Comment, error) {
287 295 c, path, err := findRequestComment(g, requestID, commentID)
288 296 if err != nil {
289 297 return nil, err
290 298 }
299 if c.Deleted {
300 return nil, fmt.Errorf("requests: comment %s is already deleted", commentID)
301 }
302 c.Deleted = true
303 c.Body = ""
304 c.Anchor = nil
305 raw, err := RenderComment(c)
306 if err != nil {
307 return nil, err
308 }
291 309 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
292 310 if _, ok := files[path]; !ok {
293 311 return fmt.Errorf("requests: no comment %s", commentID)
294 312 }
295 delete(files, path)
313 files[path] = raw
296 314 return nil
297 315 }, fmt.Sprintf("request: delete comment %s on %s", commentID, requestID), actorOr(actor, c.Author))
298 316 if err != nil {