1package daemon23import (4 "errors"5 "strconv"6 "strings"78 "github.com/FelineStateMachine/tinyrelay/internal/event"9)1011// Review comments. A kind 1111 comment under a pull request or patch may name12// 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. The14// relay validates the shape whenever the tags are present and browse results15// carry the anchor as plain fields so clients can place the comment.1617const reviewFileMax = 40961819// reviewAnchor is the diff line a comment names. A zero value means the20// 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}2627// collaborationReply is one comment in a thread with its anchor, when it has28// one, and its proposal state alongside the event fields.29type collaborationReply struct {30 event.Event31 reviewAnchor32 collaborationProposal33}3435// collaborationReplies pairs each reply with its anchor and the proposal36// 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 out48}4950// reviewAnchorOf reads a comment's anchor. It reports whether the tags are51// 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, nil55 }56 var files, lines [][]string57 for _, tag := range e.Tags {58 if len(tag) == 0 {59 continue60 }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, nil70 }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, nil93}9495// validateReviewAnchor is the admission check for comments that carry file96// and line tags. Comments without them pass.97func validateReviewAnchor(e event.Event) error {98 _, _, err := reviewAnchorOf(e)99 return err100}