tinyrelayfile
tinyrelay/internal/daemon/collaboration_review.go/

internal/daemon/collaboration_review.go raw
1package daemon
2
3import (
4 "errors"
5 "strconv"
6 "strings"
7
8 "github.com/FelineStateMachine/tinyrelay/internal/event"
9)
10
11// Review comments. A kind 1111 comment under a pull request or patch may name
12// one line of the diff with ["file", "<path>"] and ["line", "<n>", "<side>"],
13// where the side is "old" for the base or "new" for the proposed version. The
14// relay validates the shape whenever the tags are present and browse results
15// carry the anchor as plain fields so clients can place the comment.
16
17const reviewFileMax = 4096
18
19// reviewAnchor is the diff line a comment names. A zero value means the
20// comment is not anchored.
21type reviewAnchor struct {
22 File string `json:"file,omitempty"`
23 Line int `json:"line,omitempty"`
24 Side string `json:"side,omitempty"`
25}
26
27// collaborationReply is one comment in a thread with its anchor, when it has
28// one, and its proposal state alongside the event fields.
29type collaborationReply struct {
30 event.Event
31 reviewAnchor
32 collaborationProposal
33}
34
35// collaborationReplies pairs each reply with its anchor and the proposal
36// state at the same index of states.
37func collaborationReplies(rows []event.Event, states []collaborationProposal) []collaborationReply {
38 out := make([]collaborationReply, 0, len(rows))
39 for i, row := range rows {
40 anchor, _, _ := reviewAnchorOf(row)
41 reply := collaborationReply{Event: row, reviewAnchor: anchor}
42 if i < len(states) {
43 reply.collaborationProposal = states[i]
44 }
45 out = append(out, reply)
46 }
47 return out
48}
49
50// reviewAnchorOf reads a comment's anchor. It reports whether the tags are
51// present and returns a blocked reason when they are malformed.
52func reviewAnchorOf(e event.Event) (reviewAnchor, bool, error) {
53 if e.Kind != 1111 {
54 return reviewAnchor{}, false, nil
55 }
56 var files, lines [][]string
57 for _, tag := range e.Tags {
58 if len(tag) == 0 {
59 continue
60 }
61 switch tag[0] {
62 case "file":
63 files = append(files, tag)
64 case "line":
65 lines = append(lines, tag)
66 }
67 }
68 if len(files) == 0 && len(lines) == 0 {
69 return reviewAnchor{}, false, nil
70 }
71 if len(files) != 1 || len(lines) != 1 {
72 return reviewAnchor{}, true, errors.New("blocked: a review comment carries one file tag and one line tag")
73 }
74 file := files[0]
75 if len(file) < 2 || strings.TrimSpace(file[1]) == "" || len(file[1]) > reviewFileMax || strings.ContainsAny(file[1], "\n\r\x00") {
76 return reviewAnchor{}, true, errors.New("blocked: a review comment file tag needs a path")
77 }
78 line := lines[0]
79 if len(line) < 3 {
80 return reviewAnchor{}, true, errors.New("blocked: a review comment line tag needs a line number and a side, old or new")
81 }
82 n, err := strconv.Atoi(line[1])
83 if err != nil || n < 1 || strconv.Itoa(n) != line[1] {
84 return reviewAnchor{}, true, errors.New("blocked: a review comment line must be a positive integer")
85 }
86 if line[2] != "old" && line[2] != "new" {
87 return reviewAnchor{}, true, errors.New("blocked: a review comment side must be old or new")
88 }
89 if kind := event.Tag(e, "K"); kind != "1617" && kind != "1618" {
90 return reviewAnchor{}, true, errors.New("blocked: a review comment anchors to a pull request or patch")
91 }
92 return reviewAnchor{File: file[1], Line: n, Side: line[2]}, true, nil
93}
94
95// validateReviewAnchor is the admission check for comments that carry file
96// and line tags. Comments without them pass.
97func validateReviewAnchor(e event.Event) error {
98 _, _, err := reviewAnchorOf(e)
99 return err
100}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs