11
11
from enum import Enum
12
12
import itertools
13
13
import operator
14
+ from collections import defaultdict
14
15
15
16
16
17
class LineStatus (Enum ):
@@ -24,7 +25,7 @@ class Issue(object):
24
25
"""Basic Issue model for collecting the necessary info to send to GitHub."""
25
26
26
27
def __init__ (self , title , labels , assignees , milestone , body , hunk , file_name ,
27
- start_line , markdown_language , status , identifier ):
28
+ start_line , markdown_language , status , identifier , ref ):
28
29
self .title = title
29
30
self .labels = labels
30
31
self .assignees = assignees
@@ -36,6 +37,7 @@ def __init__(self, title, labels, assignees, milestone, body, hunk, file_name,
36
37
self .markdown_language = markdown_language
37
38
self .status = status
38
39
self .identifier = identifier
40
+ self .ref = ref
39
41
40
42
41
43
class GitHubClient (object ):
@@ -133,15 +135,22 @@ def create_issue(self, issue):
133
135
issue_contents = formatted_issue_body + '\n \n ' + url_to_line + '\n \n ' + snippet
134
136
else :
135
137
issue_contents = url_to_line + '\n \n ' + snippet
136
- # Check if the current issue already exists - if so, skip it.
137
- # The below is a simple and imperfect check based on the issue title.
138
- for existing_issue in self .existing_issues :
139
- if issue .title == existing_issue ['title' ]:
140
- print (f'Skipping issue (already exists).' )
141
- return
142
138
143
139
new_issue_body = {'title' : title , 'body' : issue_contents , 'labels' : issue .labels }
144
140
141
+ issues_url = self .issues_url
142
+ if issue .ref :
143
+ if issue .ref .startswith ('@' ):
144
+ issue .assignees .append (issue .ref .lstrip ('@' ))
145
+ elif issue .ref .startswith ('#' ):
146
+ issue_num = issue .ref .lstrip ('#' )
147
+ if issue_num .isdigit ():
148
+ issues_url += f'/{ issue_num } '
149
+ elif issue .ref .startswith ('!' ):
150
+ issue .labels .append (issue .ref .lstrip ('!' ))
151
+ else :
152
+ issue .title = f'[{ issue .ref } ] { issue .title } '
153
+
145
154
# We need to check if any assignees/milestone specified exist, otherwise issue creation will fail.
146
155
valid_assignees = []
147
156
if len (issue .assignees ) == 0 and self .auto_assign :
@@ -163,7 +172,7 @@ def create_issue(self, issue):
163
172
else :
164
173
print (f'Milestone { issue .milestone } does not exist! Dropping this parameter!' )
165
174
166
- new_issue_request = requests .post (url = self . issues_url , headers = self .issue_headers ,
175
+ new_issue_request = requests .post (url = issues_url , headers = self .issue_headers ,
167
176
data = json .dumps (new_issue_body ))
168
177
169
178
return new_issue_request .status_code
@@ -423,7 +432,7 @@ def parse(self, diff_file):
423
432
extracted_comments = []
424
433
prev_comment = None
425
434
for i , comment in enumerate (comments ):
426
- if i == 0 or re .search ('|' .join (self .identifiers ), comment .group (0 )):
435
+ if i == 0 or re .search ('|' .join (self .identifiers ), comment .group (0 ), re . IGNORECASE ):
427
436
extracted_comments .append ([comment ])
428
437
else :
429
438
if comment .start () == prev_comment .end () + 1 :
@@ -493,12 +502,8 @@ def _extract_issue_if_exists(self, comment, marker, code_block):
493
502
cleaned_line = self ._clean_line (committed_line , marker )
494
503
line_title , ref , identifier = self ._get_title (cleaned_line )
495
504
if line_title :
496
- if ref :
497
- issue_title = f'[{ ref } ] { line_title } '
498
- else :
499
- issue_title = line_title
500
505
issue = Issue (
501
- title = issue_title ,
506
+ title = line_title ,
502
507
labels = ['todo' ],
503
508
assignees = [],
504
509
milestone = None ,
@@ -508,7 +513,8 @@ def _extract_issue_if_exists(self, comment, marker, code_block):
508
513
start_line = code_block ['start_line' ],
509
514
markdown_language = code_block ['markdown_language' ],
510
515
status = line_status ,
511
- identifier = identifier
516
+ identifier = identifier ,
517
+ ref = ref
512
518
)
513
519
514
520
# Calculate the file line number that this issue references.
@@ -623,7 +629,7 @@ def _get_title(self, comment):
623
629
title_pattern = re .compile (r'(?<=' + identifier + r'[\s:]).+' , re .IGNORECASE )
624
630
title_search = title_pattern .search (comment , re .IGNORECASE )
625
631
if title_search :
626
- title = title_search .group (0 ).strip ()
632
+ title = title_search .group (0 ).strip (': ' )
627
633
break
628
634
else :
629
635
title_ref_pattern = re .compile (r'(?<=' + identifier + r'\().+' , re .IGNORECASE )
@@ -710,13 +716,40 @@ def _should_ignore(self, file):
710
716
f'Assuming this issue has been moved so skipping.' )
711
717
continue
712
718
issues_to_process .extend (similar_issues )
719
+
720
+ # If a TODO with an issue number reference is updated, it will appear as an addition and deletion.
721
+ # We need to ignore the deletion so it doesn't update then immediately close the issue.
722
+ # First store TODOs based on their status.
723
+ todos_status = defaultdict (lambda : {'added' : False , 'deleted' : False })
724
+
725
+ # Populate the status dictionary.
726
+ for raw_issue in issues_to_process :
727
+ if raw_issue .ref and raw_issue .ref .startswith ('#' ):
728
+ if raw_issue .status == LineStatus .ADDED :
729
+ todos_status [raw_issue .ref ]['added' ] = True
730
+ elif raw_issue .status == LineStatus .DELETED :
731
+ todos_status [raw_issue .ref ]['deleted' ] = True
732
+
733
+ # Determine which issues are both added and deleted.
734
+ update_and_close_issues = set ()
735
+
736
+ for reference , status in todos_status .items ():
737
+ if status ['added' ] and status ['deleted' ]:
738
+ update_and_close_issues .add (reference )
739
+
740
+ # Remove issues from issues_to_process if they are both to be updated and closed.
741
+ issues_to_process = [issue for issue in issues_to_process if
742
+ not (issue .ref in update_and_close_issues and issue .status == LineStatus .DELETED )]
743
+
713
744
# Cycle through the Issue objects and create or close a corresponding GitHub issue for each.
714
745
for j , raw_issue in enumerate (issues_to_process ):
715
746
print (f'Processing issue { j + 1 } of { len (issues_to_process )} ' )
716
747
if raw_issue .status == LineStatus .ADDED :
717
748
status_code = client .create_issue (raw_issue )
718
749
if status_code == 201 :
719
750
print ('Issue created' )
751
+ elif status_code == 200 :
752
+ print ('Issue updated' )
720
753
else :
721
754
print ('Issue could not be created' )
722
755
elif raw_issue .status == LineStatus .DELETED and os .getenv ('INPUT_CLOSE_ISSUES' , 'true' ) == 'true' :
0 commit comments