2
2
3
3
import re
4
4
from dataclasses import dataclass
5
- from datetime import datetime
6
5
from enum import Enum
7
6
from typing import NamedTuple
8
7
9
- from django .conf import settings
10
- from django .utils import timezone
11
8
from gidgethub import sansio
12
9
13
- from .conf import app_settings
14
-
15
10
16
11
class EventAction (NamedTuple ):
17
12
event : str
@@ -76,8 +71,6 @@ class RawMention:
76
71
CODE_BLOCK_PATTERN = re .compile (r"```[\s\S]*?```" , re .MULTILINE )
77
72
INLINE_CODE_PATTERN = re .compile (r"`[^`]+`" )
78
73
BLOCKQUOTE_PATTERN = re .compile (r"^\s*>.*$" , re .MULTILINE )
79
-
80
-
81
74
# GitHub username rules:
82
75
# - 1-39 characters long
83
76
# - Can only contain alphanumeric characters or hyphens
@@ -127,63 +120,47 @@ def for_mention_in_comment(cls, comment: str, mention_position: int):
127
120
return cls (lineno = line_number , text = line_text )
128
121
129
122
130
- def extract_mention_text (
131
- body : str , current_index : int , all_mentions : list [RawMention ], mention_end : int
132
- ) -> str :
133
- text_start = mention_end
134
-
135
- # Find next @mention (any mention, not just matched ones) to know where this text ends
136
- next_mention_index = None
137
- for j in range (current_index + 1 , len (all_mentions )):
138
- next_mention_index = j
139
- break
140
-
141
- if next_mention_index is not None :
142
- text_end = all_mentions [next_mention_index ].position
143
- else :
144
- text_end = len (body )
145
-
146
- return body [text_start :text_end ].strip ()
147
-
148
-
149
123
@dataclass
150
124
class ParsedMention :
151
125
username : str
152
- text : str
153
126
position : int
154
127
line_info : LineInfo
155
- match : re .Match [str ] | None = None
156
128
previous_mention : ParsedMention | None = None
157
129
next_mention : ParsedMention | None = None
158
130
159
131
132
+ def matches_pattern (text : str , pattern : str | re .Pattern [str ]) -> bool :
133
+ match pattern :
134
+ case re .Pattern ():
135
+ return pattern .fullmatch (text ) is not None
136
+ case str ():
137
+ return text .strip ().lower () == pattern .strip ().lower ()
138
+
139
+
160
140
def extract_mentions_from_event (
161
141
event : sansio .Event , username_pattern : str | re .Pattern [str ] | None = None
162
142
) -> list [ParsedMention ]:
163
- comment = event .data .get ("comment" , {}).get ("body" , "" )
143
+ comment_key = "comment" if event .event != "pull_request_review" else "review"
144
+ comment = event .data .get (comment_key , {}).get ("body" , "" )
164
145
165
146
if not comment :
166
147
return []
167
148
168
- if username_pattern is None :
169
- username_pattern = app_settings .SLUG
170
-
171
149
mentions : list [ParsedMention ] = []
172
150
potential_mentions = extract_all_mentions (comment )
173
- for i , raw_mention in enumerate (potential_mentions ):
174
- if not matches_pattern (raw_mention .username , username_pattern ):
151
+ for raw_mention in potential_mentions :
152
+ if username_pattern and not matches_pattern (
153
+ raw_mention .username , username_pattern
154
+ ):
175
155
continue
176
156
177
- text = extract_mention_text (comment , i , potential_mentions , raw_mention .end )
178
- line_info = LineInfo .for_mention_in_comment (comment , raw_mention .position )
179
-
180
157
mentions .append (
181
158
ParsedMention (
182
159
username = raw_mention .username ,
183
- text = text ,
184
160
position = raw_mention .position ,
185
- line_info = line_info ,
186
- match = None ,
161
+ line_info = LineInfo .for_mention_in_comment (
162
+ comment , raw_mention .position
163
+ ),
187
164
previous_mention = None ,
188
165
next_mention = None ,
189
166
)
@@ -198,63 +175,8 @@ def extract_mentions_from_event(
198
175
return mentions
199
176
200
177
201
- @dataclass
202
- class Comment :
203
- body : str
204
- author : str
205
- created_at : datetime
206
- url : str
207
- mentions : list [ParsedMention ]
208
-
209
- @property
210
- def line_count (self ) -> int :
211
- if not self .body :
212
- return 0
213
- return len (self .body .splitlines ())
214
-
215
- @classmethod
216
- def from_event (cls , event : sansio .Event ) -> Comment :
217
- match event .event :
218
- case "issue_comment" | "pull_request_review_comment" | "commit_comment" :
219
- comment_data = event .data .get ("comment" )
220
- case "pull_request_review" :
221
- comment_data = event .data .get ("review" )
222
- case _:
223
- comment_data = None
224
-
225
- if not comment_data :
226
- raise ValueError (f"Cannot extract comment from event type: { event .event } " )
227
-
228
- if created_at_str := comment_data .get ("created_at" , "" ):
229
- # GitHub timestamps are in ISO format: 2024-01-01T12:00:00Z
230
- created_at_aware = datetime .fromisoformat (
231
- created_at_str .replace ("Z" , "+00:00" )
232
- )
233
- if settings .USE_TZ :
234
- created_at = created_at_aware
235
- else :
236
- created_at = timezone .make_naive (
237
- created_at_aware , timezone .get_default_timezone ()
238
- )
239
- else :
240
- created_at = timezone .now ()
241
-
242
- author = comment_data .get ("user" , {}).get ("login" , "" )
243
- if not author and "sender" in event .data :
244
- author = event .data .get ("sender" , {}).get ("login" , "" )
245
-
246
- return cls (
247
- body = comment_data .get ("body" , "" ),
248
- author = author ,
249
- created_at = created_at ,
250
- url = comment_data .get ("html_url" , "" ),
251
- mentions = [],
252
- )
253
-
254
-
255
178
@dataclass
256
179
class Mention :
257
- comment : Comment
258
180
mention : ParsedMention
259
181
scope : MentionScope | None
260
182
@@ -264,50 +186,8 @@ def from_event(
264
186
event : sansio .Event ,
265
187
* ,
266
188
username : str | re .Pattern [str ] | None = None ,
267
- pattern : str | re .Pattern [str ] | None = None ,
268
189
scope : MentionScope | None = None ,
269
190
):
270
- event_scope = MentionScope .from_event (event )
271
- if scope is not None and event_scope != scope :
272
- return
273
-
274
191
mentions = extract_mentions_from_event (event , username )
275
- if not mentions :
276
- return
277
-
278
- comment = Comment .from_event (event )
279
- comment .mentions = mentions
280
-
281
192
for mention in mentions :
282
- if pattern is not None :
283
- match = get_match (mention .text , pattern )
284
- if not match :
285
- continue
286
- mention .match = match
287
-
288
- yield cls (
289
- comment = comment ,
290
- mention = mention ,
291
- scope = event_scope ,
292
- )
293
-
294
-
295
- def matches_pattern (text : str , pattern : str | re .Pattern [str ]) -> bool :
296
- match pattern :
297
- case re .Pattern ():
298
- return pattern .fullmatch (text ) is not None
299
- case str ():
300
- return text .strip ().lower () == pattern .strip ().lower ()
301
-
302
-
303
- def get_match (text : str , pattern : str | re .Pattern [str ] | None ) -> re .Match [str ] | None :
304
- match pattern :
305
- case None :
306
- return re .match (r"(.*)" , text , re .IGNORECASE | re .DOTALL )
307
- case re .Pattern ():
308
- # Use the pattern directly, preserving its flags
309
- return pattern .match (text )
310
- case str ():
311
- # For strings, do exact match (case-insensitive)
312
- # Escape the string to treat it literally
313
- return re .match (re .escape (pattern ), text , re .IGNORECASE )
193
+ yield cls (mention = mention , scope = scope )
0 commit comments