]> git.proxmox.com Git - ceph.git/blob - ceph/src/script/backport-resolve-issue
2501f103ec2a3d72514a3138b17a33ed9d64160e
[ceph.git] / ceph / src / script / backport-resolve-issue
1 #!/usr/bin/env python3
2 #
3 # backport-resolve-issue
4 #
5 # Based on "backport-create-issue", which was itself based on work by
6 # by Loic Dachary.
7 #
8 #
9 # Introduction
10 # ============
11 #
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.
16 #
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
20 # PR and terminates.
21 #
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".
26 #
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
32 # PR number for each.
33 #
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.
37 #
38 #
39 # Assumptions
40 # ===========
41 #
42 # Among other things, the script assumes:
43 #
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
46 # tab is "firefox"
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
51 # in a web browser
52 #
53 #
54 # Dependencies
55 # ============
56 #
57 # To run this script, first install the dependencies
58 #
59 # virtualenv v
60 # source v/bin/activate
61 # pip install gitpython python-redmine
62 #
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
65 # PATH.
66 #
67 # Finally, run the script with appropriate parameters. For example:
68 #
69 # backport-resolve-issue --key $MY_REDMINE_KEY
70 # backport-resolve-issue --user $MY_REDMINE_USER --password $MY_REDMINE_PASSWORD
71 #
72 #
73 # Copyright Notice
74 # ================
75 #
76 # Copyright (C) 2019, SUSE LLC
77 #
78 # Author: Nathan Cutler <ncutler@suse.com>
79 #
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.
84 #
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.
89 #
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/>
92 #
93 import argparse
94 import logging
95 import json
96 import os
97 import re
98 import sys
99 import time
100 from redminelib import Redmine # https://pypi.org/project/python-redmine/
101 from redminelib.exceptions import ResourceAttrError
102 from git import Repo
103 from git.exc import GitCommandError
104
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 = {}
112 delay_seconds = 5
113 browser_cmd = "firefox"
114 no_browser = False
115 ceph_release = None
116 dry_run = False
117 redmine = None
118 bri_tag = None
119 github_token_file = "~/.github_token"
120 github_token = None
121 github_user = None
122 redmine_key_file = "~/.redmine_key"
123 redmine_key = None
124
125 def browser_running():
126 global browser_cmd
127 retval = os.system("pgrep {} >/dev/null".format(browser_cmd))
128 if retval == 0:
129 return True
130 return False
131
132 def ceph_version(repo, sha1=None):
133 if sha1:
134 return repo.git.describe('--match', 'v*', sha1).split('-')[0]
135 return repo.git.describe('--match', 'v*').split('-')[0]
136
137 def commit_range(args):
138 global bri_tag
139 if len(args.pr_or_commit) == 0:
140 return '{}..HEAD'.format(bri_tag)
141 elif len(args.pr_or_commit) == 1:
142 pass
143 else:
144 logging.warn("Ignoring positional parameters {}".format(args.pr_or_commit[1:]))
145 commit = args.pr_or_commit[0]
146 return '{}..HEAD'.format(commit)
147
148 def connect_to_redmine(a):
149 global redmine_key
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)
155 elif redmine_key:
156 logging.info("Redmine key was read from '%s'; using it" % redmine_key_file)
157 return Redmine(redmine_endpoint, key=redmine_key)
158 else:
159 usage()
160
161 def derive_github_user_from_token(gh_token):
162 retval = None
163 if 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:
172 assert False, \
173 "GitHub API unexpectedly returned ->{}<-".format(github_api_result['message'])
174 return retval
175
176 def ensure_bri_tag_exists(repo, release):
177 global bri_tag
178 bri_tag = "BRI-{}".format(release)
179 bri_tag_exists = ''
180 try:
181 bri_tag_exists = repo.git.show_ref(bri_tag)
182 except GitCommandError as err:
183 logging.error(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)
189
190 def get_issue_release(redmine_issue):
191 for field in redmine_issue.custom_fields:
192 if field['name'] == 'Release':
193 return field['value']
194 return None
195
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]
201
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:
205 return True
206 return False
207
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",
213 action="store_true")
214 parser.add_argument("--dry-run", help="Do not write anything to Redmine",
215 action="store_true")
216 parser.add_argument("--no-browser", help="Do not use web browser even if it is running",
217 action="store_true")
218 parser.add_argument("pr_or_commit", nargs='*',
219 help="GitHub PR ID, or last merge commit successfully processed")
220 return parser.parse_args()
221
222 def populate_ceph_release(repo):
223 global ceph_release
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]
228 try:
229 ceph_release = ver_to_release()[release_ver]
230 except KeyError:
231 assert False, \
232 "Release version {} does not correspond to any known stable release".format(release_ver)
233 logging.info("Ceph release is {}".format(ceph_release))
234
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))
239 return None
240
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))
245 return None
246
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
252 return None
253
254 def print_inner_divider():
255 print("-----------------------------------------------------------------")
256
257 def print_outer_divider():
258 print("=================================================================")
259
260 def process_merge(repo, merge, merges_remaining):
261 backport = None
262 sha1 = merge.split(' ')[0]
263 possible_to_resolve = True
264 try:
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') --> "
274 .format(bri=bri_tag)
275 )
276 default_input_val = "u"
277 else:
278 prompt = ("[a] Abort, "
279 "[i] Ignore and advance {bri} tag (default 'i') --> "
280 .format(bri=bri_tag)
281 )
282 default_input_val = "i"
283 else:
284 if possible_to_resolve:
285 prompt = "[a] Abort, [i] Ignore, [u] Update tracker (default 'u') --> "
286 default_input_val = "u"
287 else:
288 if merges_remaining > 1:
289 prompt = "[a] Abort, [i] Ignore --> "
290 default_input_val = "i"
291 else:
292 return False
293 input_val = input(prompt)
294 if input_val == '':
295 input_val = default_input_val
296 if input_val.lower() == "a":
297 exit(-1)
298 elif input_val.lower() == "i":
299 pass
300 else:
301 input_val = "u"
302 if input_val.lower() == "u":
303 if backport:
304 backport.resolve()
305 else:
306 logging.warn("Cannot determine which issue to resolve. Ignoring.")
307 if tag_merge_commits:
308 if backport:
309 tag_sha1(repo, backport.merge_commit_sha1)
310 else:
311 tag_sha1(repo, sha1)
312 return True
313
314 def read_from_file(fs):
315 retval = None
316 full_path = os.path.expanduser(fs)
317 try:
318 with open(full_path, "r") as f:
319 retval = f.read().strip()
320 except FileNotFoundError:
321 pass
322 return retval
323
324 def releases():
325 return ('argonaut', 'bobtail', 'cuttlefish', 'dumpling', 'emperor',
326 'firefly', 'giant', 'hammer', 'infernalis', 'jewel', 'kraken',
327 'luminous', 'mimic', 'nautilus', 'octopus', 'pacific')
328
329 def report_params(a):
330 global dry_run
331 global no_browser
332 if a.dry_run:
333 dry_run = True
334 logging.warning("Dry run: nothing will be written to Redmine")
335 if a.no_browser:
336 no_browser = True
337 logging.warning("Web browser will not be used even if it is running")
338
339 def set_logging_level(a):
340 if a.debug:
341 logging.basicConfig(level=logging.DEBUG)
342 else:
343 logging.basicConfig(level=logging.INFO)
344 return None
345
346 def tag_sha1(repo, sha1):
347 global bri_tag
348 repo.git.tag('--delete', bri_tag)
349 repo.git.tag(bri_tag, sha1)
350
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',
355 'v16.2': 'pacific'}
356
357 def usage():
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)
366 )
367 exit(-1)
368
369
370 class Backport:
371
372 def __init__(self, repo, merge_commit_string):
373 '''
374 The merge commit string should look something like this:
375 27ff851953 Merge pull request #29678 from pdvian/wip-40948-nautilus
376 '''
377 global browser_cmd
378 global ceph_release
379 global github_token
380 global github_user
381 self.repo = repo
382 self.merge_commit_string = merge_commit_string
383 #
384 # split merge commit string on first space character
385 merge_commit_sha1_short, self.merge_commit_description = merge_commit_string.split(' ', 1)
386 #
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)
393 )
394 logging.debug("Short merge commit SHA1 is {}".format(self.merge_commit_sha1_short))
395 self.merge_commit_sha1 = self.repo.git.rev_list(
396 '--max-count=1',
397 self.merge_commit_sha1_short,
398 )
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()
404 #
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)
410 cmd = (
411 "curl {} https://api.github.com/repos/ceph/ceph/pulls/{}"
412 .format(curl_opt, self.github_pr_id)
413 )
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"]
420 else:
421 logging.error("GitHub API unexpectedly returned: {}".format(github_api_result))
422 logging.info("Curl command was: {}".format(cmd))
423 sys.exit(-1)
424 self.mogrify_github_pr_desc()
425 self.github_pr_merged = github_api_result["merged"]
426 if not no_browser:
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=================================================================
433 GitHub PR URL: {}
434 GitHub PR title: {}
435 Merge commit: {} ({})
436 Merged: {}
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,
440 self.target_version
441 )
442 )
443 if no_browser or not browser_running():
444 print('''----------------------- PR DESCRIPTION --------------------------
445 {}
446 -----------------------------------------------------------------'''.format(self.github_pr_desc))
447 assert self.github_pr_merged, "GitHub PR {} has not been merged!".format(self.github_pr_id)
448 #
449 # obtain backport tracker from GitHub PR description
450 self.extract_backport_trackers_from_github_pr_desc()
451 #
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()
456 try:
457 bt.github_url_from_tracker = p.search(bt.tracker_description).group()
458 except AttributeError:
459 pass
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(
467 self.github_pr_id,
468 bt.github_id_from_tracker,
469 )
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))
473 else:
474 logging.warning("Backport Tracker {} does not link to PR - will update"
475 .format(bt.issue_id))
476 #
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, \
480 (
481 "Backport Tracker {} is a {} backport - expected {}"
482 .format(bt.issue_id, tracker_release, ceph_release)
483 )
484 #
485 # is the Backport Tracker's "Target version" custom field populated?
486 try:
487 ttv = bt.get_tracker_target_version()
488 except:
489 logging.info("Backport Tracker {} target version not populated yet!"
490 .format(bt.issue_id))
491 bt.set_target_version = True
492 else:
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, \
499 (
500 "Tracker target version {} is wrong; should be {}"
501 .format(bt.tracker_target_version, self.target_version)
502 )
503 #
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
510 else:
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()
515
516 def populate_base_version(self):
517 self.base_version = ceph_version(self.repo, self.merge_commit_sha1)
518
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
527 else:
528 raise Exception("Version {} not found in Redmine".format(tv))
529
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")
537 else:
538 self.github_pr_desc = new_str
539
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+)')
544 try:
545 self.github_pr_id = p.search(self.merge_commit_description).group(2)
546 except AttributeError:
547 assert False, \
548 (
549 "Failed to extract GitHub PR ID from merge commit string ->{}<-"
550 .format(self.merge_commit_string)
551 )
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)
555
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()
562 assert False, \
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()
568 if not issue_id:
569 print_outer_divider()
570 assert issue_id, \
571 "Failed to extract tracker ID from tracker URL {}".format(issue_url)
572 issue_url = "{}/issues/{}".format(redmine_endpoint, issue_id)
573 #
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)
580 )
581 print('''Found backport tracker: {}'''.format(issue_url))
582 if not self.backport_trackers:
583 print_outer_divider()
584 assert False, \
585 "No backport tracker found in PR description at {}".format(self.github_url)
586
587 def resolve(self):
588 for bt in self.backport_trackers:
589 bt.resolve()
590
591
592 class BackportTracker(Backport):
593
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
600
601 def get_tracker_description(self):
602 try:
603 self.tracker_description = self.redmine_issue.description
604 except ResourceAttrError:
605 self.tracker_description = ""
606
607 def get_tracker_target_version(self):
608 if self.redmine_issue.fixed_version:
609 logging.debug("Target version: ID {}, name {}"
610 .format(
611 self.redmine_issue.fixed_version.id,
612 self.redmine_issue.fixed_version.name
613 )
614 )
615 return self.redmine_issue.fixed_version.name
616 return None
617
618 def issue_url(self):
619 return "{}/issues/{}".format(redmine_endpoint, self.issue_id)
620
621 def resolve(self):
622 global delay_seconds
623 global dry_run
624 global redmine
625 kwargs = {}
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,
635 )
636 else:
637 kwargs['description'] = self.parent.github_url
638 kwargs['notes'] = (
639 "This update was made using the script \"backport-resolve-issue\".\n"
640 "backport PR {}\n"
641 "merge commit {} ({})\n".format(
642 self.parent.github_url,
643 self.parent.merge_commit_sha1,
644 self.parent.merge_commit_gd,
645 )
646 )
647 my_delay_seconds = delay_seconds
648 if dry_run:
649 logging.info("--dry-run was given: NOT updating Redmine")
650 my_delay_seconds = 0
651 else:
652 logging.debug("Updating tracker ID {}".format(self.issue_id))
653 redmine.issue.update(self.issue_id, **kwargs)
654 if not no_browser:
655 if browser_running():
656 os.system("{} {}".format(browser_cmd, self.issue_url()))
657 my_delay_seconds = 3
658 logging.debug(
659 "Delaying {} seconds to avoid seeming like a spammer"
660 .format(my_delay_seconds)
661 )
662 time.sleep(my_delay_seconds)
663
664
665 if __name__ == '__main__':
666 args = parse_arguments()
667 set_logging_level(args)
668 logging.debug(args)
669 github_token = read_from_file(github_token_file)
670 if github_token:
671 logging.info("GitHub token was read from ->{}<-; using it".format(github_token_file))
672 github_user = derive_github_user_from_token(github_token)
673 if github_user:
674 logging.info(
675 "GitHub user ->{}<- was derived from the GitHub token".format(github_user)
676 )
677 report_params(args)
678 #
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"]
686 logging.debug(
687 "Pending Backport status has ID {}"
688 .format(pending_backport_status_id)
689 )
690 populate_tracker_dict(redmine)
691 populate_version_dict(redmine, ceph_project_id)
692 #
693 # construct github Repo object for the current directory
694 repo = Repo('.')
695 assert not repo.bare
696 populate_ceph_release(repo)
697 #
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]
701 try:
702 pr_id = int(pr_id)
703 logging.info("Examining PR#{}".format(pr_id))
704 tag_merge_commits = False
705 except ValueError:
706 logging.info("Starting from merge commit {}".format(args.pr_or_commit))
707 tag_merge_commits = True
708 else:
709 logging.info("Starting from BRI tag")
710 tag_merge_commits = True
711 #
712 # get list of merges
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))
717 #
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')
721 else:
722 pr_id = args.pr_or_commit[0]
723 merges_raw_str = repo.git.log(
724 '--merges',
725 '--grep=#{}'.format(pr_id),
726 '--oneline',
727 '--no-decorate',
728 '--reverse',
729 )
730 if merges_raw_str:
731 merges_raw_list = merges_raw_str.split('\n')
732 else:
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))
740 #
741 # loop over the merge commits
742 for merge in merges_raw_list:
743 can_go_on = process_merge(repo, merge, merges_remaining)
744 if can_go_on:
745 merges_remaining -= 1
746 print("Merges remaining to process: {}".format(merges_remaining))
747 else:
748 break