3 # backport-resolve-issue
5 # Based on "backport-create-issue", which was itself based on work by
12 # This script processes GitHub backport PRs, checking for proper cross-linking
13 # with a Redmine Backport tracker issue and, if a PR is merged and properly
14 # cross-linked, it can optionally resolve the tracker issue and correctly
15 # populate the "Target version" field.
17 # The script takes a single positional argument, which is optional. If the
18 # argument is an integer, it is assumed to be a GitHub backport PR ID (e.g. "28549").
19 # In this mode ("single PR mode") the script processes a single GitHub backport
22 # If the argument is not an integer, or is missing, it is assumed to be a
23 # commit (SHA1 or tag) to start from. If no positional argument is given, it
24 # defaults to the tag "BRI-{release}", which might have been added by the last run of the
25 # script. This mode is called "scan merge commits mode".
27 # In both modes, the script scans a local git repo, which is assumed to be
28 # in the current working directory. In single PR mode, the script will work
29 # only if the PR's merge commit is present in the current branch of the local
30 # git repo. In scan merge commits mode, the script starts from the given SHA1
31 # or tag, taking each merge commit in turn and attempting to obtain the GitHub
34 # For each GitHub PR, the script interactively displays all relevant information
35 # (NOTE: this includes displaying the GitHub PR and Redmine backport issue in
36 # web browser tabs!) and prompts the user for her preferred disposition.
42 # Among other things, the script assumes:
44 # 1. it is being run in the top-level directory of a Ceph git repo
45 # 2. the preferred web browser is Firefox and the command to open a browser
47 # 3. if Firefox is running and '--no-browser' was not given, the Firefox window
48 # is visible to the user and the user desires to view GitHub PRs and Tracker
49 # Issues in the browser
50 # 4. if Firefox is not running, the user does not want to view PRs and issues
57 # To run this script, first install the dependencies
60 # source v/bin/activate
61 # pip install gitpython python-redmine
63 # Then, copy the script from src/script/backport-resolve-issue (in the branch
64 # "master" - the script is not maintained anywhere else) to somewhere in your
67 # Finally, run the script with appropriate parameters. For example:
69 # backport-resolve-issue --key $MY_REDMINE_KEY
70 # backport-resolve-issue --user $MY_REDMINE_USER --password $MY_REDMINE_PASSWORD
76 # Copyright (C) 2019, SUSE LLC
78 # Author: Nathan Cutler <ncutler@suse.com>
80 # This program is free software: you can redistribute it and/or modify
81 # it under the terms of the GNU Affero General Public License as
82 # published by the Free Software Foundation, either version 3 of the
83 # License, or (at your option) any later version.
85 # This program is distributed in the hope that it will be useful,
86 # but WITHOUT ANY WARRANTY; without even the implied warranty of
87 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
88 # GNU Affero General Public License for more details.
90 # You should have received a copy of the GNU Affero General Public License
91 # along with this program. If not, see http://www.gnu.org/licenses/>
100 from redminelib
import Redmine
# https://pypi.org/project/python-redmine/
101 from redminelib
.exceptions
import ResourceAttrError
103 from git
.exc
import GitCommandError
105 github_endpoint
= "https://github.com/ceph/ceph"
106 redmine_endpoint
= "https://tracker.ceph.com"
107 project_name
= "Ceph"
108 status2status_id
= {}
109 project_id2project
= {}
110 tracker2tracker_id
= {}
111 version2version_id
= {}
113 browser_cmd
= "firefox"
119 github_token_file
= "~/.github_token"
122 redmine_key_file
= "~/.redmine_key"
125 def browser_running():
127 retval
= os
.system("pgrep {} >/dev/null".format(browser_cmd
))
132 def ceph_version(repo
, sha1
=None):
134 return repo
.git
.describe('--match', 'v*', sha1
).split('-')[0]
135 return repo
.git
.describe('--match', 'v*').split('-')[0]
137 def commit_range(args
):
139 if len(args
.pr_or_commit
) == 0:
140 return '{}..HEAD'.format(bri_tag
)
141 elif len(args
.pr_or_commit
) == 1:
144 logging
.warn("Ignoring positional parameters {}".format(args
.pr_or_commit
[1:]))
145 commit
= args
.pr_or_commit
[0]
146 return '{}..HEAD'.format(commit
)
148 def connect_to_redmine(a
):
150 global redmine_key_file
151 redmine_key
= read_from_file(redmine_key_file
)
152 if a
.user
and a
.password
:
153 logging
.info("Redmine username and password were provided; using them")
154 return Redmine(redmine_endpoint
, username
=a
.user
, password
=a
.password
)
156 logging
.info("Redmine key was read from '%s'; using it" % redmine_key_file
)
157 return Redmine(redmine_endpoint
, key
=redmine_key
)
161 def derive_github_user_from_token(gh_token
):
164 curl_opt
= "-u :{} --silent".format(gh_token
)
165 cmd
= "curl {} https://api.github.com/user".format(curl_opt
)
166 logging
.debug("Running curl command ->{}<-".format(cmd
))
167 json_str
= os
.popen(cmd
).read()
168 github_api_result
= json
.loads(json_str
)
169 if "login" in github_api_result
:
170 retval
= github_api_result
['login']
171 if "message" in github_api_result
:
173 "GitHub API unexpectedly returned ->{}<-".format(github_api_result
['message'])
176 def ensure_bri_tag_exists(repo
, release
):
178 bri_tag
= "BRI-{}".format(release
)
181 bri_tag_exists
= repo
.git
.show_ref(bri_tag
)
182 except GitCommandError
as err
:
184 logging
.debug("git show-ref {} returned ->{}<-".format(bri_tag
, bri_tag_exists
))
185 if not bri_tag_exists
:
186 c_v
= ceph_version(repo
)
187 logging
.info("No {} tag found: setting it to {}".format(bri_tag
, c_v
))
188 repo
.git
.tag(bri_tag
, c_v
)
190 def get_issue_release(redmine_issue
):
191 for field
in redmine_issue
.custom_fields
:
192 if field
['name'] == 'Release':
193 return field
['value']
196 def get_project(r
, p_id
):
197 if p_id
not in project_id2project
:
198 p_obj
= r
.project
.get(p_id
, include
='trackers')
199 project_id2project
[p_id
] = p_obj
200 return project_id2project
[p_id
]
202 def has_tracker(r
, p_id
, tracker_name
):
203 for tracker
in get_project(r
, p_id
).trackers
:
204 if tracker
['name'] == tracker_name
:
208 def parse_arguments():
209 parser
= argparse
.ArgumentParser()
210 parser
.add_argument("--user", help="Redmine user")
211 parser
.add_argument("--password", help="Redmine password")
212 parser
.add_argument("--debug", help="Show debug-level messages",
214 parser
.add_argument("--dry-run", help="Do not write anything to Redmine",
216 parser
.add_argument("--no-browser", help="Do not use web browser even if it is running",
218 parser
.add_argument("pr_or_commit", nargs
='*',
219 help="GitHub PR ID, or last merge commit successfully processed")
220 return parser
.parse_args()
222 def populate_ceph_release(repo
):
224 current_branch
= repo
.git
.rev_parse('--abbrev-ref', 'HEAD')
225 release_ver_full
= ceph_version(repo
)
226 logging
.info("Current git branch is {}, {}".format(current_branch
, release_ver_full
))
227 release_ver
= release_ver_full
.split('.')[0] + '.' + release_ver_full
.split('.')[1]
229 ceph_release
= ver_to_release()[release_ver
]
232 "Release version {} does not correspond to any known stable release".format(release_ver
)
233 logging
.info("Ceph release is {}".format(ceph_release
))
235 def populate_status_dict(r
):
236 for status
in r
.issue_status
.all():
237 status2status_id
[status
.name
] = status
.id
238 logging
.debug("Statuses {}".format(status2status_id
))
241 def populate_tracker_dict(r
):
242 for tracker
in r
.tracker
.all():
243 tracker2tracker_id
[tracker
.name
] = tracker
.id
244 logging
.debug("Trackers {}".format(tracker2tracker_id
))
247 # not used currently, but might be useful
248 def populate_version_dict(r
, p_id
):
249 versions
= r
.version
.filter(project_id
=p_id
)
250 for version
in versions
:
251 version2version_id
[version
.name
] = version
.id
254 def print_inner_divider():
255 print("-----------------------------------------------------------------")
257 def print_outer_divider():
258 print("=================================================================")
260 def process_merge(repo
, merge
, merges_remaining
):
262 sha1
= merge
.split(' ')[0]
263 possible_to_resolve
= True
265 backport
= Backport(repo
, merge_commit_string
=merge
)
266 except AssertionError as err
:
267 logging
.error("Malformed backport due to ->{}<-".format(err
))
268 possible_to_resolve
= False
269 if tag_merge_commits
:
270 if possible_to_resolve
:
271 prompt
= ("[a] Abort, "
272 "[i] Ignore and advance {bri} tag, "
273 "[u] Update tracker and advance {bri} tag (default 'u') --> "
276 default_input_val
= "u"
278 prompt
= ("[a] Abort, "
279 "[i] Ignore and advance {bri} tag (default 'i') --> "
282 default_input_val
= "i"
284 if possible_to_resolve
:
285 prompt
= "[a] Abort, [i] Ignore, [u] Update tracker (default 'u') --> "
286 default_input_val
= "u"
288 if merges_remaining
> 1:
289 prompt
= "[a] Abort, [i] Ignore --> "
290 default_input_val
= "i"
293 input_val
= input(prompt
)
295 input_val
= default_input_val
296 if input_val
.lower() == "a":
298 elif input_val
.lower() == "i":
302 if input_val
.lower() == "u":
306 logging
.warn("Cannot determine which issue to resolve. Ignoring.")
307 if tag_merge_commits
:
309 tag_sha1(repo
, backport
.merge_commit_sha1
)
314 def read_from_file(fs
):
316 full_path
= os
.path
.expanduser(fs
)
318 with
open(full_path
, "r") as f
:
319 retval
= f
.read().strip()
320 except FileNotFoundError
:
325 return ('argonaut', 'bobtail', 'cuttlefish', 'dumpling', 'emperor',
326 'firefly', 'giant', 'hammer', 'infernalis', 'jewel', 'kraken',
327 'luminous', 'mimic', 'nautilus', 'octopus', 'pacific')
329 def report_params(a
):
334 logging
.warning("Dry run: nothing will be written to Redmine")
337 logging
.warning("Web browser will not be used even if it is running")
339 def set_logging_level(a
):
341 logging
.basicConfig(level
=logging
.DEBUG
)
343 logging
.basicConfig(level
=logging
.INFO
)
346 def tag_sha1(repo
, sha1
):
348 repo
.git
.tag('--delete', bri_tag
)
349 repo
.git
.tag(bri_tag
, sha1
)
351 def ver_to_release():
352 return {'v9.2': 'infernalis', 'v10.2': 'jewel', 'v11.2': 'kraken',
353 'v12.2': 'luminous', 'v13.2': 'mimic', 'v14.2': 'nautilus',
354 'v15.2': 'octopus', 'v16.0': 'pacific', 'v16.1': 'pacific',
358 logging
.error("Redmine credentials are required to perform this operation. "
359 "Please provide either a Redmine key (via {}) "
360 "or a Redmine username and password (via --user and --password). "
361 "Optionally, one or more issue numbers can be given via positional "
362 "argument(s). In the absence of positional arguments, the script "
363 "will loop through all merge commits after the tag \"BRI-{release}\". "
364 "If there is no such tag in the local branch, one will be created "
365 "for you.".format(redmine_key_file
)
372 def __init__(self
, repo
, merge_commit_string
):
374 The merge commit string should look something like this:
375 27ff851953 Merge pull request #29678 from pdvian/wip-40948-nautilus
382 self
.merge_commit_string
= merge_commit_string
384 # split merge commit string on first space character
385 merge_commit_sha1_short
, self
.merge_commit_description
= merge_commit_string
.split(' ', 1)
387 # merge commit SHA1 from merge commit string
388 p
= re
.compile('\\S+')
389 self
.merge_commit_sha1_short
= p
.match(merge_commit_sha1_short
).group()
390 assert self
.merge_commit_sha1_short
== merge_commit_sha1_short
, \
391 ("Failed to extract merge commit short SHA1 from merge commit string ->{}<-"
392 .format(merge_commit_string
)
394 logging
.debug("Short merge commit SHA1 is {}".format(self
.merge_commit_sha1_short
))
395 self
.merge_commit_sha1
= self
.repo
.git
.rev_list(
397 self
.merge_commit_sha1_short
,
399 logging
.debug("Full merge commit SHA1 is {}".format(self
.merge_commit_sha1
))
400 self
.merge_commit_gd
= repo
.git
.describe('--match', 'v*', self
.merge_commit_sha1
)
401 self
.populate_base_version()
402 self
.populate_target_version()
403 self
.populate_github_url()
405 # GitHub PR description and merged status from GitHub
406 curl_opt
= "--silent"
407 # if GitHub token was provided, use it to avoid throttling -
408 if github_token
and github_user
:
409 curl_opt
= "-u {}:{} {}".format(github_user
, github_token
, curl_opt
)
411 "curl {} https://api.github.com/repos/ceph/ceph/pulls/{}"
412 .format(curl_opt
, self
.github_pr_id
)
414 logging
.debug("Running curl command ->{}<-".format(cmd
))
415 json_str
= os
.popen(cmd
).read()
416 github_api_result
= json
.loads(json_str
)
417 if "title" in github_api_result
and "body" in github_api_result
:
418 self
.github_pr_title
= github_api_result
["title"]
419 self
.github_pr_desc
= github_api_result
["body"]
421 logging
.error("GitHub API unexpectedly returned: {}".format(github_api_result
))
422 logging
.info("Curl command was: {}".format(cmd
))
424 self
.mogrify_github_pr_desc()
425 self
.github_pr_merged
= github_api_result
["merged"]
427 if browser_running():
428 os
.system("{} {}".format(browser_cmd
, self
.github_url
))
429 pr_title_trunc
= self
.github_pr_title
430 if len(pr_title_trunc
) > 60:
431 pr_title_trunc
= pr_title_trunc
[0:50] + "|TRUNCATED"
432 print('''\n\n=================================================================
435 Merge commit: {} ({})
437 Ceph version: base {}, target {}'''
438 .format(self
.github_url
, pr_title_trunc
, self
.merge_commit_sha1
,
439 self
.merge_commit_gd
, self
.github_pr_merged
, self
.base_version
,
443 if no_browser
or not browser_running():
444 print('''----------------------- PR DESCRIPTION --------------------------
446 -----------------------------------------------------------------'''.format(self
.github_pr_desc
))
447 assert self
.github_pr_merged
, "GitHub PR {} has not been merged!".format(self
.github_pr_id
)
449 # obtain backport tracker from GitHub PR description
450 self
.extract_backport_trackers_from_github_pr_desc()
452 for bt
in self
.backport_trackers
:
453 # does the Backport Tracker description link back to the GitHub PR?
454 p
= re
.compile('http.?://github.com/ceph/ceph/pull/\\d+')
455 bt
.get_tracker_description()
457 bt
.github_url_from_tracker
= p
.search(bt
.tracker_description
).group()
458 except AttributeError:
460 if bt
.github_url_from_tracker
:
461 p
= re
.compile('\\d+')
462 bt
.github_id_from_tracker
= p
.search(bt
.github_url_from_tracker
).group()
463 logging
.debug("GitHub PR from Tracker: URL is ->{}<- and ID is {}"
464 .format(bt
.github_url_from_tracker
, bt
.github_id_from_tracker
))
465 assert bt
.github_id_from_tracker
== self
.github_pr_id
, \
466 "GitHub PR ID {} does not match GitHub ID from tracker {}".format(
468 bt
.github_id_from_tracker
,
470 print_inner_divider()
471 if bt
.github_url_from_tracker
:
472 logging
.info("Tracker {} links to PR {}".format(bt
.issue_url(), self
.github_url
))
474 logging
.warning("Backport Tracker {} does not link to PR - will update"
475 .format(bt
.issue_id
))
477 # does the Backport Tracker's release field match the Ceph release?
478 tracker_release
= get_issue_release(bt
.redmine_issue
)
479 assert ceph_release
== tracker_release
, \
481 "Backport Tracker {} is a {} backport - expected {}"
482 .format(bt
.issue_id
, tracker_release
, ceph_release
)
485 # is the Backport Tracker's "Target version" custom field populated?
487 ttv
= bt
.get_tracker_target_version()
489 logging
.info("Backport Tracker {} target version not populated yet!"
490 .format(bt
.issue_id
))
491 bt
.set_target_version
= True
493 bt
.tracker_target_version
= ttv
494 logging
.info("Backport Tracker {} target version already populated "
495 "with correct value {}"
496 .format(bt
.issue_id
, bt
.tracker_target_version
))
497 bt
.set_target_version
= False
498 assert bt
.tracker_target_version
== self
.target_version
, \
500 "Tracker target version {} is wrong; should be {}"
501 .format(bt
.tracker_target_version
, self
.target_version
)
504 # is the Backport Tracker's status already set to Resolved?
505 resolved_id
= status2status_id
['Resolved']
506 if bt
.redmine_issue
.status
.id == resolved_id
:
507 logging
.info("Backport Tracker {} status is already set to Resolved"
508 .format(bt
.issue_id
))
509 bt
.set_tracker_status
= False
511 logging
.info("Backport Tracker {} status is currently set to {}"
512 .format(bt
.issue_id
, bt
.redmine_issue
.status
))
513 bt
.set_tracker_status
= True
514 print_outer_divider()
516 def populate_base_version(self
):
517 self
.base_version
= ceph_version(self
.repo
, self
.merge_commit_sha1
)
519 def populate_target_version(self
):
520 x
, y
, z
= self
.base_version
.split('v')[1].split('.')
521 maybe_stable
= "v{}.{}".format(x
, y
)
522 assert ver_to_release()[maybe_stable
], \
523 "SHA1 {} is not based on any known stable release ({})".format(sha1
, maybe_stable
)
524 tv
= "v{}.{}.{}".format(x
, y
, int(z
) + 1)
525 if tv
in version2version_id
:
526 self
.target_version
= tv
528 raise Exception("Version {} not found in Redmine".format(tv
))
530 def mogrify_github_pr_desc(self
):
531 if not self
.github_pr_desc
:
532 self
.github_pr_desc
= ''
533 p
= re
.compile('<!--.+-->', re
.DOTALL
)
534 new_str
= p
.sub('', self
.github_pr_desc
)
535 if new_str
== self
.github_pr_desc
:
536 logging
.debug("GitHub PR description not mogrified")
538 self
.github_pr_desc
= new_str
540 def populate_github_url(self
):
541 global github_endpoint
542 # GitHub PR ID from merge commit string
543 p
= re
.compile('(pull request|PR) #(\\d+)')
545 self
.github_pr_id
= p
.search(self
.merge_commit_description
).group(2)
546 except AttributeError:
549 "Failed to extract GitHub PR ID from merge commit string ->{}<-"
550 .format(self
.merge_commit_string
)
552 logging
.debug("Merge commit string: {}".format(self
.merge_commit_string
))
553 logging
.debug("GitHub PR ID from merge commit string: {}".format(self
.github_pr_id
))
554 self
.github_url
= "{}/pull/{}".format(github_endpoint
, self
.github_pr_id
)
556 def extract_backport_trackers_from_github_pr_desc(self
):
557 global redmine_endpoint
558 p
= re
.compile('http.?://tracker.ceph.com/issues/\\d+')
559 matching_strings
= p
.findall(self
.github_pr_desc
)
560 if not matching_strings
:
561 print_outer_divider()
563 "GitHub PR description does not contain a Tracker URL"
564 self
.backport_trackers
= []
565 for issue_url
in list(dict.fromkeys(matching_strings
)):
566 p
= re
.compile('\\d+')
567 issue_id
= p
.search(issue_url
).group()
569 print_outer_divider()
571 "Failed to extract tracker ID from tracker URL {}".format(issue_url
)
572 issue_url
= "{}/issues/{}".format(redmine_endpoint
, issue_id
)
574 # we have a Tracker URL, but is it really a backport tracker?
575 backport_tracker_id
= tracker2tracker_id
['Backport']
576 redmine_issue
= redmine
.issue
.get(issue_id
)
577 if redmine_issue
.tracker
.id == backport_tracker_id
:
578 self
.backport_trackers
.append(
579 BackportTracker(redmine_issue
, issue_id
, self
)
581 print('''Found backport tracker: {}'''.format(issue_url
))
582 if not self
.backport_trackers
:
583 print_outer_divider()
585 "No backport tracker found in PR description at {}".format(self
.github_url
)
588 for bt
in self
.backport_trackers
:
592 class BackportTracker(Backport
):
594 def __init__(self
, redmine_issue
, issue_id
, backport_obj
):
595 self
.redmine_issue
= redmine_issue
596 self
.issue_id
= issue_id
597 self
.parent
= backport_obj
598 self
.tracker_description
= None
599 self
.github_url_from_tracker
= None
601 def get_tracker_description(self
):
603 self
.tracker_description
= self
.redmine_issue
.description
604 except ResourceAttrError
:
605 self
.tracker_description
= ""
607 def get_tracker_target_version(self
):
608 if self
.redmine_issue
.fixed_version
:
609 logging
.debug("Target version: ID {}, name {}"
611 self
.redmine_issue
.fixed_version
.id,
612 self
.redmine_issue
.fixed_version
.name
615 return self
.redmine_issue
.fixed_version
.name
619 return "{}/issues/{}".format(redmine_endpoint
, self
.issue_id
)
626 if self
.set_tracker_status
:
627 kwargs
['status_id'] = status2status_id
['Resolved']
628 if self
.set_target_version
:
629 kwargs
['fixed_version_id'] = version2version_id
[self
.parent
.target_version
]
630 if not self
.github_url_from_tracker
:
631 if self
.tracker_description
:
632 kwargs
['description'] = "{}\n\n---\n\n{}".format(
633 self
.parent
.github_url
,
634 self
.tracker_description
,
637 kwargs
['description'] = self
.parent
.github_url
639 "This update was made using the script \"backport-resolve-issue\".\n"
641 "merge commit {} ({})\n".format(
642 self
.parent
.github_url
,
643 self
.parent
.merge_commit_sha1
,
644 self
.parent
.merge_commit_gd
,
647 my_delay_seconds
= delay_seconds
649 logging
.info("--dry-run was given: NOT updating Redmine")
652 logging
.debug("Updating tracker ID {}".format(self
.issue_id
))
653 redmine
.issue
.update(self
.issue_id
, **kwargs
)
655 if browser_running():
656 os
.system("{} {}".format(browser_cmd
, self
.issue_url()))
659 "Delaying {} seconds to avoid seeming like a spammer"
660 .format(my_delay_seconds
)
662 time
.sleep(my_delay_seconds
)
665 if __name__
== '__main__':
666 args
= parse_arguments()
667 set_logging_level(args
)
669 github_token
= read_from_file(github_token_file
)
671 logging
.info("GitHub token was read from ->{}<-; using it".format(github_token_file
))
672 github_user
= derive_github_user_from_token(github_token
)
675 "GitHub user ->{}<- was derived from the GitHub token".format(github_user
)
679 # set up Redmine variables
680 redmine
= connect_to_redmine(args
)
681 project
= redmine
.project
.get(project_name
)
682 ceph_project_id
= project
.id
683 logging
.debug("Project {} has ID {}".format(project_name
, ceph_project_id
))
684 populate_status_dict(redmine
)
685 pending_backport_status_id
= status2status_id
["Pending Backport"]
687 "Pending Backport status has ID {}"
688 .format(pending_backport_status_id
)
690 populate_tracker_dict(redmine
)
691 populate_version_dict(redmine
, ceph_project_id
)
693 # construct github Repo object for the current directory
696 populate_ceph_release(repo
)
698 # if positional argument is an integer, assume it is a GitHub PR
699 if args
.pr_or_commit
:
700 pr_id
= args
.pr_or_commit
[0]
703 logging
.info("Examining PR#{}".format(pr_id
))
704 tag_merge_commits
= False
706 logging
.info("Starting from merge commit {}".format(args
.pr_or_commit
))
707 tag_merge_commits
= True
709 logging
.info("Starting from BRI tag")
710 tag_merge_commits
= True
713 if tag_merge_commits
:
714 ensure_bri_tag_exists(repo
, ceph_release
)
715 c_r
= commit_range(args
)
716 logging
.info("Commit range is {}".format(c_r
))
718 # get the list of merge commits, i.e. strings that looks like:
719 # "27ff851953 Merge pull request #29678 from pdvian/wip-40948-nautilus"
720 merges_raw_str
= repo
.git
.log(c_r
, '--merges', '--oneline', '--no-decorate', '--reverse')
722 pr_id
= args
.pr_or_commit
[0]
723 merges_raw_str
= repo
.git
.log(
725 '--grep=#{}'.format(pr_id
),
731 merges_raw_list
= merges_raw_str
.split('\n')
733 merges_raw_list
= [] # prevent ['']
734 merges_remaining
= len(merges_raw_list
)
735 logging
.info("I see {} merge(s) to process".format(merges_remaining
))
736 if not merges_remaining
:
737 logging
.info("Did you do \"git pull\" before running the script?")
738 if not tag_merge_commits
:
739 logging
.info("Or maybe GitHub PR {} has not been merged yet?".format(pr_id
))
741 # loop over the merge commits
742 for merge
in merges_raw_list
:
743 can_go_on
= process_merge(repo
, merge
, merges_remaining
)
745 merges_remaining
-= 1
746 print("Merges remaining to process: {}".format(merges_remaining
))