|
1 | 1 | package webhooks |
2 | 2 |
|
3 | | -import ( |
4 | | - "regexp" |
5 | | - "strconv" |
6 | | - "strings" |
7 | | - |
8 | | - "github.com/bit-issues/backend/internal/tasks" |
9 | | -) |
10 | | - |
11 | | -const ( |
12 | | - verbResolved = "Resolved" |
13 | | - verbClosed = "Closed" |
14 | | - verbOnHold = "On Hold" |
15 | | -) |
16 | | - |
17 | | -// keywordActions is the registry of recognized keywords. |
18 | | -// Extend this map to add new auto-transition keywords. |
19 | | -// |
20 | | -//nolint:gochecknoglobals // this is a constant |
21 | | -var keywordActions = map[string]KeywordAction{ |
22 | | - "fix": {Status: tasks.StatusResolved, Verb: verbResolved}, |
23 | | - "fixes": {Status: tasks.StatusResolved, Verb: verbResolved}, |
24 | | - "fixed": {Status: tasks.StatusResolved, Verb: verbResolved}, |
25 | | - "resolve": {Status: tasks.StatusResolved, Verb: verbResolved}, |
26 | | - "resolves": {Status: tasks.StatusResolved, Verb: verbResolved}, |
27 | | - "resolved": {Status: tasks.StatusResolved, Verb: verbResolved}, |
28 | | - "close": {Status: tasks.StatusClosed, Verb: verbClosed}, |
29 | | - "closes": {Status: tasks.StatusClosed, Verb: verbClosed}, |
30 | | - "closed": {Status: tasks.StatusClosed, Verb: verbClosed}, |
31 | | - "block": {Status: tasks.StatusOnHold, Verb: verbOnHold}, |
32 | | - "blocks": {Status: tasks.StatusOnHold, Verb: verbOnHold}, |
33 | | - "blocked": {Status: tasks.StatusOnHold, Verb: verbOnHold}, |
34 | | - "on hold": {Status: tasks.StatusOnHold, Verb: verbOnHold}, |
35 | | -} |
36 | | - |
37 | | -// KeywordAction maps a commit message keyword to a task status transition. |
38 | | -type KeywordAction struct { |
39 | | - Status tasks.Status // target status |
40 | | - Verb string // past-tense label for comment, e.g. "Resolved" |
41 | | -} |
42 | | - |
43 | | -var ( |
44 | | - // keywordRefPattern matches patterns like "fixes #123", "closes #456", etc. |
45 | | - keywordRefPattern = regexp.MustCompile( |
46 | | - `(?i)\b(fix|fixes|fixed|resolve|resolves|resolved|close|closes|closed|block|blocks|blocked|on hold)\s+#(\d+)\b`, |
47 | | - ) |
48 | | - // hashRefPattern matches "#NUMBER" preceded by start-of-string or a non-word character. |
49 | | - hashRefPattern = regexp.MustCompile(`(?:^|\W)#(\d+)\b`) |
50 | | -) |
51 | | - |
52 | | -type matchRange struct{ start, end int } |
53 | | - |
54 | | -func (r matchRange) overlaps(other matchRange) bool { |
55 | | - return r.start < other.end && other.start < r.end |
56 | | -} |
57 | | - |
58 | | -// ParsedReference represents a single task reference found in a commit message. |
59 | | -type ParsedReference struct { |
60 | | - TaskNumber int |
61 | | - Action *KeywordAction // nil if bare #N with no keyword |
62 | | - CommitHash string |
63 | | - CommitMessage string |
64 | | -} |
65 | | - |
66 | | -// ParseCommitMessage scans a commit message for task references. |
67 | | -// Returns a list of ParsedReference, one per unique #NUMBER found. |
68 | | -// If both a keyword and bare reference exist for the same number, the keyword wins. |
69 | | -func ParseCommitMessage(message string) []ParsedReference { |
70 | | - keywordMatches := keywordRefPattern.FindAllStringSubmatchIndex(message, -1) |
71 | | - |
72 | | - var keywordRanges []matchRange |
73 | | - var refs []ParsedReference |
74 | | - seen := make(map[int]bool) |
75 | | - |
76 | | - for _, m := range keywordMatches { |
77 | | - keyword := strings.ToLower(message[m[2]:m[3]]) |
78 | | - numStr := message[m[4]:m[5]] |
79 | | - number, _ := strconv.Atoi(numStr) |
80 | | - |
81 | | - if seen[number] { |
82 | | - continue |
83 | | - } |
84 | | - seen[number] = true |
85 | | - |
86 | | - keywordRanges = append(keywordRanges, matchRange{start: m[0], end: m[1]}) |
87 | | - |
88 | | - action := keywordActions[keyword] |
89 | | - refs = append(refs, ParsedReference{ |
90 | | - TaskNumber: number, |
91 | | - Action: &KeywordAction{Status: action.Status, Verb: action.Verb}, |
92 | | - CommitHash: "", |
93 | | - CommitMessage: message, |
94 | | - }) |
95 | | - } |
96 | | - |
97 | | - // Find #NUMBER references not part of keyword matches |
98 | | - hashMatches := hashRefPattern.FindAllStringSubmatchIndex(message, -1) |
99 | | - for _, m := range hashMatches { |
100 | | - numStr := message[m[2]:m[3]] |
101 | | - number, _ := strconv.Atoi(numStr) |
102 | | - |
103 | | - if seen[number] { |
104 | | - continue |
105 | | - } |
106 | | - seen[number] = true |
107 | | - |
108 | | - hr := matchRange{start: m[0], end: m[1]} |
109 | | - consumed := false |
110 | | - for _, kr := range keywordRanges { |
111 | | - if kr.overlaps(hr) { |
112 | | - consumed = true |
113 | | - break |
114 | | - } |
115 | | - } |
116 | | - if consumed { |
117 | | - continue |
118 | | - } |
119 | | - |
120 | | - refs = append(refs, ParsedReference{ |
121 | | - TaskNumber: number, |
122 | | - Action: nil, |
123 | | - CommitHash: "", |
124 | | - CommitMessage: message, |
125 | | - }) |
126 | | - } |
127 | | - |
128 | | - return refs |
129 | | -} |
| 3 | +import "strings" |
130 | 4 |
|
131 | 5 | // PushCommit is a flattened commit used by the service layer. |
132 | 6 | type PushCommit struct { |
|
0 commit comments