1# -*- coding: utf-8 -*- 2from __future__ import print_function 3 4COPYRIGHT = """\ 5Copyright (C) 2011-2012 OpenStack LLC. 6 7Licensed under the Apache License, Version 2.0 (the "License"); 8you may not use this file except in compliance with the License. 9You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13Unless required by applicable law or agreed to in writing, software 14distributed under the License is distributed on an "AS IS" BASIS, 15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 16implied. 17 18See the License for the specific language governing permissions and 19limitations under the License.""" 20 21import argparse 22import datetime 23import getpass 24import json 25import os 26import re 27import shlex 28import six 29import subprocess 30import sys 31import textwrap 32 33import pkg_resources 34import requests 35from six.moves import configparser 36from six.moves import input as do_input 37from six.moves.urllib.parse import urlencode 38from six.moves.urllib.parse import urljoin 39from six.moves.urllib.parse import urlparse 40 41 42VERBOSE = False 43UPDATE = False 44LOCAL_MODE = 'GITREVIEW_LOCAL_MODE' in os.environ 45CONFIGDIR = os.path.expanduser("~/.config/git-review") 46GLOBAL_CONFIG = "/etc/git-review/git-review.conf" 47USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf") 48DEFAULTS = dict(scheme='ssh', hostname=False, port=None, project=False, 49 branch='master', remote="gerrit", rebase="1", 50 track="0", usepushurl="0") 51 52_branch_name = None 53_has_color = None 54_use_color = None 55_orig_head = None 56_rewrites = None 57_rewrites_push = None 58 59 60class colors(object): 61 yellow = '\033[33m' 62 green = '\033[92m' 63 reset = '\033[0m' 64 blue = '\033[36m' 65 66 67class GitReviewException(Exception): 68 EXIT_CODE = 1 69 70 71class CommandFailed(GitReviewException): 72 73 def __init__(self, *args): 74 Exception.__init__(self, *args) 75 (self.rc, self.output, self.argv, self.envp) = args 76 self.quickmsg = dict([ 77 ("argv", " ".join(self.argv)), 78 ("rc", self.rc), 79 ("output", self.output)]) 80 81 def __str__(self): 82 return self.__doc__ + """ 83The following command failed with exit code %(rc)d 84 "%(argv)s" 85----------------------- 86%(output)s 87-----------------------""" % self.quickmsg 88 89 90class ChangeSetException(GitReviewException): 91 92 def __init__(self, e): 93 GitReviewException.__init__(self) 94 self.e = str(e) 95 96 def __str__(self): 97 return self.__doc__ % self.e 98 99 100def printwrap(unwrapped): 101 print('\n'.join(textwrap.wrap(unwrapped))) 102 103 104def warn(warning): 105 printwrap("WARNING: %s" % warning) 106 107 108def parse_review_number(review): 109 parts = review.split(',') 110 if len(parts) < 2: 111 parts.append(None) 112 return parts 113 114 115def build_review_number(review, patchset): 116 if patchset is not None: 117 return '%s,%s' % (review, patchset) 118 return review 119 120 121def run_command_status(*argv, **kwargs): 122 if VERBOSE: 123 print(datetime.datetime.now(), "Running:", " ".join(argv)) 124 if len(argv) == 1: 125 # for python2 compatibility with shlex 126 if sys.version_info < (3,) and isinstance(argv[0], six.text_type): 127 argv = shlex.split(argv[0].encode('utf-8')) 128 else: 129 argv = shlex.split(str(argv[0])) 130 stdin = kwargs.pop('stdin', None) 131 newenv = os.environ.copy() 132 newenv['LANG'] = 'C' 133 newenv['LANGUAGE'] = 'C' 134 newenv.update(kwargs) 135 p = subprocess.Popen(argv, 136 stdin=subprocess.PIPE if stdin else None, 137 stdout=subprocess.PIPE, 138 stderr=subprocess.STDOUT, 139 env=newenv, universal_newlines=True) 140 (out, nothing) = p.communicate(stdin) 141 return (p.returncode, out.strip()) 142 143 144def run_command(*argv, **kwargs): 145 (rc, output) = run_command_status(*argv, **kwargs) 146 return output 147 148 149def run_command_exc(klazz, *argv, **env): 150 """Run command *argv, on failure raise klazz 151 152 klazz should be derived from CommandFailed 153 """ 154 (rc, output) = run_command_status(*argv, **env) 155 if rc: 156 raise klazz(rc, output, argv, env) 157 return output 158 159 160def git_credentials(url): 161 """Return credentials using git credential or None.""" 162 cmd = 'git', 'credential', 'fill' 163 stdin = 'url=%s' % url 164 rc, out = run_command_status(*cmd, stdin=stdin.encode('utf-8')) 165 if rc: 166 return None 167 data = dict(l.split('=', 1) for l in out.splitlines()) 168 return data['username'], data['password'] 169 170 171def http_code_2_return_code(code): 172 """Tranform http status code to system return code.""" 173 return (code - 301) % 255 + 1 174 175 176def run_http_exc(klazz, url, **env): 177 """Run http GET request url, on failure raise klazz 178 179 klazz should be derived from CommandFailed 180 """ 181 if url.startswith("https://") and "verify" not in env: 182 if "GIT_SSL_NO_VERIFY" in os.environ: 183 env["verify"] = False 184 else: 185 verify = git_config_get_value("http", "sslVerify", as_bool=True) 186 env["verify"] = verify != 'false' 187 188 try: 189 res = requests.get(url, **env) 190 if res.status_code == 401: 191 creds = git_credentials(url) 192 if creds: 193 env['auth'] = creds 194 res = requests.get(url, **env) 195 except klazz: 196 raise 197 except Exception as err: 198 raise klazz(255, str(err), ('GET', url), env) 199 if not 200 <= res.status_code < 300: 200 raise klazz(http_code_2_return_code(res.status_code), 201 res.text, ('GET', url), env) 202 return res 203 204 205def get_version(): 206 requirement = pkg_resources.Requirement.parse('git-review') 207 provider = pkg_resources.get_provider(requirement) 208 return provider.version 209 210 211def git_directories(): 212 """Determine (absolute git work directory path, .git subdirectory path).""" 213 cmd = ("git", "rev-parse", "--show-toplevel", "--git-dir") 214 out = run_command_exc(GitDirectoriesException, *cmd) 215 try: 216 return out.splitlines() 217 except ValueError: 218 raise GitDirectoriesException(0, out, cmd, {}) 219 220 221class GitDirectoriesException(CommandFailed): 222 "Cannot determine where .git directory is." 223 EXIT_CODE = 70 224 225 226class CustomScriptException(CommandFailed): 227 """Custom script execution failed.""" 228 EXIT_CODE = 71 229 230 231def run_custom_script(action): 232 """Get status and output of .git/hooks/$action-review or/and 233 ~/.config/hooks/$action-review if existing. 234 """ 235 returns = [] 236 script_file = "%s-review" % (action) 237 (top_dir, git_dir) = git_directories() 238 paths = [os.path.join(CONFIGDIR, "hooks", script_file), 239 os.path.join(git_dir, "hooks", script_file)] 240 for fpath in paths: 241 if os.path.isfile(fpath) and os.access(fpath, os.X_OK): 242 status, output = run_command_status(fpath) 243 returns.append((status, output, fpath)) 244 245 for (status, output, path) in returns: 246 if status: 247 raise CustomScriptException(status, output, [path], {}) 248 elif output and VERBOSE: 249 print("script %s output is:" % (path)) 250 print(output) 251 252 253def git_config_get_value(section, option, default=None, as_bool=False): 254 """Get config value for section/option.""" 255 cmd = ["git", "config", "--get", "%s.%s" % (section, option)] 256 if as_bool: 257 cmd.insert(2, "--bool") 258 if LOCAL_MODE: 259 __, git_dir = git_directories() 260 cmd[2:2] = ['-f', os.path.join(git_dir, 'config')] 261 try: 262 result = run_command_exc(GitConfigException, *cmd).strip() 263 if VERBOSE: 264 print(datetime.datetime.now(), "... %s.%s = %s" 265 % (section, option, result)) 266 return result 267 except GitConfigException as exc: 268 if exc.rc == 1: 269 if VERBOSE and default is not None: 270 print(datetime.datetime.now(), 271 "... nothing in git config, returning func parameter:", 272 default) 273 return default 274 raise 275 276 277class Config(object): 278 """Expose as dictionary configuration options.""" 279 280 def __init__(self, config_file=None): 281 self.config = DEFAULTS.copy() 282 filenames = [] if LOCAL_MODE else [GLOBAL_CONFIG, USER_CONFIG] 283 if config_file: 284 filenames.append(config_file) 285 for filename in filenames: 286 if os.path.exists(filename): 287 if filename != config_file: 288 msg = ("Using global/system git-review config files (%s) " 289 "is deprecated and will be removed in a future " 290 "release") 291 warn(msg % filename) 292 self.config.update(load_config_file(filename)) 293 294 def __getitem__(self, key): 295 """Let 'git config --get' override every Config['key'] access""" 296 value = git_config_get_value('gitreview', key) 297 if value is None: 298 value = self.config[key] 299 # "--verbose" doesn't trace *early* invocations; for that you 300 # must change the value at the top of this file (*and* pass 301 # --verbose) 302 if VERBOSE: 303 print(datetime.datetime.now(), 304 "Config['%s'] = %s " % (key, value)) 305 return value 306 307 308class GitConfigException(CommandFailed): 309 """Git config value retrieval failed.""" 310 EXIT_CODE = 128 311 312 313class CannotInstallHook(CommandFailed): 314 "Problems encountered installing commit-msg hook" 315 EXIT_CODE = 2 316 317 318def set_hooks_commit_msg(remote, target_file): 319 """Install the commit message hook if needed.""" 320 321 # Create the hooks directory if it's not there already 322 hooks_dir = os.path.dirname(target_file) 323 if not os.path.isdir(hooks_dir): 324 os.mkdir(hooks_dir) 325 326 if not os.path.exists(target_file) or UPDATE: 327 remote_url = get_remote_url(remote) 328 if (remote_url.startswith('http://') or 329 remote_url.startswith('https://')): 330 hook_url = urljoin(remote_url, '/tools/hooks/commit-msg') 331 if VERBOSE: 332 print("Fetching commit hook from: %s" % hook_url) 333 res = run_http_exc(CannotInstallHook, hook_url, stream=True) 334 with open(target_file, 'wb') as f: 335 for x in res.iter_content(1024): 336 f.write(x) 337 else: 338 (hostname, username, port, project_name) = \ 339 parse_gerrit_ssh_params_from_git_url(remote_url) 340 if username: 341 userhost = "%s@%s" % (username, hostname) 342 else: 343 userhost = hostname 344 # OS independent target file 345 scp_target_file = target_file.replace(os.sep, "/") 346 cmd = ["scp", userhost + ":hooks/commit-msg", scp_target_file] 347 if port is not None: 348 cmd.insert(1, "-P%s" % port) 349 350 if VERBOSE: 351 hook_url = 'scp://%s%s/hooks/commit-msg' \ 352 % (userhost, (":%s" % port) if port else "") 353 print("Fetching commit hook from: %s" % hook_url) 354 run_command_exc(CannotInstallHook, *cmd) 355 356 if not os.access(target_file, os.X_OK): 357 os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC) 358 359 360def test_remote_url(remote_url): 361 """Tests that a possible gerrit remote url works.""" 362 status, description = run_command_status("git", "push", "--dry-run", 363 remote_url, "--all") 364 if status != 128: 365 if VERBOSE: 366 print("%s worked. Description: %s" % (remote_url, description)) 367 return True 368 else: 369 print("%s did not work. Description: %s" % (remote_url, description)) 370 return False 371 372 373def make_remote_url(scheme, username, hostname, port, project): 374 """Builds a gerrit remote URL.""" 375 if port is None and scheme == 'ssh': 376 port = 29418 377 hostport = '%s:%s' % (hostname, port) if port else hostname 378 if username: 379 return "%s://%s@%s/%s" % (scheme, username, hostport, project) 380 else: 381 return "%s://%s/%s" % (scheme, hostport, project) 382 383 384def add_remote(scheme, hostname, port, project, remote, usepushurl): 385 """Adds a gerrit remote.""" 386 asked_for_username = False 387 388 username = git_config_get_value("gitreview", "username") 389 if not username: 390 username = getpass.getuser() 391 392 remote_url = make_remote_url(scheme, username, hostname, port, project) 393 if VERBOSE: 394 print("No remote set, testing %s" % remote_url) 395 if not test_remote_url(remote_url): 396 print("Could not connect to gerrit.") 397 username = do_input("Enter your gerrit username: ") 398 remote_url = make_remote_url(scheme, username, hostname, port, project) 399 print("Trying again with %s" % remote_url) 400 if not test_remote_url(remote_url): 401 raise GerritConnectionException( 402 "Could not connect to gerrit at %s" % remote_url 403 ) 404 asked_for_username = True 405 406 if usepushurl: 407 cmd = "git remote set-url --push %s %s" % (remote, remote_url) 408 print("Adding a git push url to '%s' that maps to:" % remote) 409 else: 410 cmd = "git remote add -f %s %s" % (remote, remote_url) 411 print("Creating a git remote called '%s' that maps to:" % remote) 412 print("\t%s" % remote_url) 413 414 (status, remote_output) = run_command_status(cmd) 415 if status: 416 raise CommandFailed(status, remote_output, cmd, {}) 417 418 if asked_for_username: 419 print() 420 printwrap("This repository is now set up for use with git-review. " 421 "You can set the default username for future repositories " 422 "with:") 423 print(' git config --global --add gitreview.username "%s"' % username) 424 print() 425 426 427def populate_rewrites(): 428 """Populate the global _rewrites and _rewrites_push maps based on the 429 output of "git-config". 430 """ 431 432 cmd = ['git', 'config', '--list'] 433 out = run_command_exc(CommandFailed, *cmd).strip() 434 435 global _rewrites, _rewrites_push 436 _rewrites = {} 437 _rewrites_push = {} 438 439 for entry in out.splitlines(): 440 key, _, value = entry.partition('=') 441 key = key.lower() 442 443 if key.startswith('url.') and key.endswith('.insteadof'): 444 rewrite = key[len('url.'):-len('.insteadof')] 445 if rewrite: 446 _rewrites[value] = rewrite 447 elif key.startswith('url.') and key.endswith('.pushinsteadof'): 448 rewrite = key[len('url.'):-len('.pushinsteadof')] 449 if rewrite: 450 _rewrites_push[value] = rewrite 451 452 453def alias_url(url, rewrite_push): 454 """Expand a remote URL. Use the global map _rewrites to replace the 455 longest match with its equivalent. If rewrite_push is True, try 456 _rewrites_push before _rewrites. 457 """ 458 459 if _rewrites is None: 460 populate_rewrites() 461 462 if rewrite_push: 463 maps = [_rewrites_push, _rewrites] 464 else: 465 maps = [_rewrites] 466 467 for rewrites in maps: 468 # If Git finds a pushInsteadOf alias, it uses that even if 469 # there is a longer insteadOf alias. 470 longest = None 471 for alias in rewrites: 472 if (url.startswith(alias) 473 and (longest is None or len(longest) < len(alias))): 474 longest = alias 475 476 if longest: 477 return url.replace(longest, rewrites[longest]) 478 479 return url 480 481 482def get_remote_url(remote): 483 """Retrieve the remote URL. Read the configuration to expand the URL of a 484 remote repository taking into account any "url.<base>.insteadOf" or 485 "url.<base>.pushInsteadOf" config setting. 486 487 TODO: Replace current code with something like "git ls-remote 488 --get-url" after Git grows a version of it that returns the push 489 URL rather than the fetch URL. 490 """ 491 492 push_url = git_config_get_value('remote.%s' % remote, 'pushurl') 493 if push_url is not None: 494 # Git rewrites pushurl using insteadOf but not pushInsteadOf. 495 push_url = alias_url(push_url, False) 496 else: 497 url = git_config_get_value('remote.%s' % remote, 'url') 498 # Git rewrites url using pushInsteadOf or insteadOf. 499 push_url = alias_url(url, True) 500 if VERBOSE: 501 print("Found origin Push URL:", push_url) 502 return push_url 503 504 505def parse_gerrit_ssh_params_from_git_url(git_url): 506 """Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either 507 real URLs or SCP-style addresses. 508 """ 509 510 # The exact code for this in Git itself is a bit obtuse, so just do 511 # something sensible and pythonic here instead of copying the exact 512 # minutiae from Git. 513 514 # Handle real(ish) URLs 515 if "://" in git_url: 516 parsed_url = urlparse(git_url) 517 path = parsed_url.path 518 519 hostname = parsed_url.netloc 520 username = None 521 port = parsed_url.port 522 523 # Workaround bug in urlparse on OSX 524 if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//": 525 hostname = parsed_url.path[2:].split("/")[0] 526 527 if "@" in hostname: 528 (username, _, hostname) = hostname.rpartition("@") 529 if ":" in hostname: 530 (hostname, port) = hostname.split(":") 531 532 if port is not None: 533 port = str(port) 534 535 # Handle SCP-style addresses 536 else: 537 username = None 538 port = None 539 (hostname, path) = git_url.split(":", 1) 540 if "@" in hostname: 541 (username, hostname) = hostname.split("@", 1) 542 543 # Strip leading slash and trailing .git from the path to form the project 544 # name. 545 project_name = re.sub(r"^/|(\.git$)", "", path) 546 547 return (hostname, username, port, project_name) 548 549 550def query_reviews(remote_url, project=None, change=None, 551 current_patch_set=True, exception=CommandFailed, 552 parse_exc=Exception): 553 if remote_url.startswith('http://') or remote_url.startswith('https://'): 554 query = query_reviews_over_http 555 else: 556 query = query_reviews_over_ssh 557 return query(remote_url, 558 project=project, 559 change=change, 560 current_patch_set=current_patch_set, 561 exception=exception, 562 parse_exc=parse_exc) 563 564 565def query_reviews_over_http(remote_url, project=None, change=None, 566 current_patch_set=True, exception=CommandFailed, 567 parse_exc=Exception): 568 if project: 569 # Remove any trailing .git suffixes for project to url comparison 570 clean_url = os.path.splitext(remote_url)[0] 571 clean_project = os.path.splitext(project)[0] 572 if clean_url.endswith(clean_project): 573 # Get the "root" url for gerrit by removing the project from the 574 # url. For example: 575 # https://example.com/foo/project.git gets truncated to 576 # https://example.com/foo/ regardless of whether or not none, 577 # either, or both of the remote_url or project strings end 578 # with .git. 579 remote_url = clean_url[:-len(clean_project)] 580 url = urljoin(remote_url, 'changes/') 581 if change: 582 if current_patch_set: 583 url += '?q=%s&o=CURRENT_REVISION' % change 584 else: 585 url += '?q=%s&o=ALL_REVISIONS' % change 586 else: 587 if project: 588 project_name = re.sub(r"^/|(\.git$)", "", 589 project) 590 else: 591 project_name = re.sub(r"^/|(\.git$)", "", 592 urlparse(remote_url).path) 593 params = urlencode({'q': 'project:%s status:open' % project_name}) 594 url += '?' + params 595 596 if VERBOSE: 597 print("Query gerrit %s" % url) 598 request = run_http_exc(exception, url) 599 if VERBOSE: 600 print(request.text) 601 reviews = json.loads(request.text[4:]) 602 603 # Reformat output to match ssh output 604 try: 605 for review in reviews: 606 review["number"] = str(review.pop("_number")) 607 if "revisions" not in review: 608 continue 609 patchsets = {} 610 for key, revision in review["revisions"].items(): 611 fetch_value = list(revision["fetch"].values())[0] 612 patchset = {"number": str(revision["_number"]), 613 "ref": fetch_value["ref"]} 614 patchsets[key] = patchset 615 review["patchSets"] = patchsets.values() 616 review["currentPatchSet"] = patchsets[review["current_revision"]] 617 except Exception as err: 618 raise parse_exc(err) 619 620 return reviews 621 622 623def query_reviews_over_ssh(remote_url, project=None, change=None, 624 current_patch_set=True, exception=CommandFailed, 625 parse_exc=Exception): 626 (hostname, username, port, project_name) = \ 627 parse_gerrit_ssh_params_from_git_url(remote_url) 628 629 if change: 630 if current_patch_set: 631 query = "--current-patch-set change:%s" % change 632 else: 633 query = "--patch-sets change:%s" % change 634 else: 635 query = "project:%s status:open" % project_name 636 637 port_data = "p%s" % port if port is not None else "" 638 if username is None: 639 userhost = hostname 640 else: 641 userhost = "%s@%s" % (username, hostname) 642 643 if VERBOSE: 644 print("Query gerrit %s %s" % (remote_url, query)) 645 output = run_command_exc( 646 exception, 647 "ssh", "-x" + port_data, userhost, 648 "gerrit", "query", 649 "--format=JSON %s" % query) 650 if VERBOSE: 651 print(output) 652 653 changes = [] 654 try: 655 for line in output.split("\n"): 656 if line[0] == "{": 657 try: 658 data = json.loads(line) 659 if "type" not in data: 660 changes.append(data) 661 except Exception: 662 if VERBOSE: 663 print(output) 664 except Exception as err: 665 raise parse_exc(err) 666 return changes 667 668 669def set_color_output(color="auto"): 670 global _use_color 671 if check_color_support(): 672 if color == "auto": 673 check_use_color_output() 674 else: 675 _use_color = color == "always" 676 677 678def check_use_color_output(): 679 global _use_color 680 if _use_color is None: 681 if check_color_support(): 682 # we can support color, now check if we should use it 683 stdout = "true" if sys.stdout.isatty() else "false" 684 test_command = "git config --get-colorbool color.review " + stdout 685 color = run_command(test_command) 686 _use_color = color == "true" 687 else: 688 _use_color = False 689 return _use_color 690 691 692def check_color_support(): 693 global _has_color 694 if _has_color is None: 695 test_command = "git log --color=never --oneline HEAD^1..HEAD" 696 (status, output) = run_command_status(test_command) 697 if status == 0: 698 _has_color = True 699 else: 700 _has_color = False 701 return _has_color 702 703 704def load_config_file(config_file): 705 """Load configuration options from a file.""" 706 configParser = configparser.ConfigParser() 707 configParser.read(config_file) 708 options = { 709 'scheme': 'scheme', 710 'hostname': 'host', 711 'port': 'port', 712 'project': 'project', 713 'branch': 'defaultbranch', 714 'remote': 'defaultremote', 715 'rebase': 'defaultrebase', 716 'track': 'track', 717 'usepushurl': 'usepushurl', 718 } 719 config = {} 720 for config_key, option_name in options.items(): 721 if configParser.has_option('gerrit', option_name): 722 config[config_key] = configParser.get('gerrit', option_name) 723 return config 724 725 726def update_remote(remote): 727 cmd = "git remote update %s" % remote 728 (status, output) = run_command_status(cmd) 729 if VERBOSE: 730 print(output) 731 if status != 0: 732 print("Problem running '%s'" % cmd) 733 if not VERBOSE: 734 print(output) 735 return False 736 return True 737 738 739def parse_tracking(ref=None): 740 """Return tracked (remote, branch) of current HEAD or other named 741 branch if tracking remote. 742 """ 743 if ref is None: 744 ref = run_command_exc( 745 SymbolicRefFailed, 746 "git", "symbolic-ref", "-q", "HEAD") 747 tracked = run_command_exc( 748 ForEachRefFailed, 749 "git", "for-each-ref", "--format=%(upstream)", ref) 750 751 # Only on explicitly tracked remote branch do we diverge from default 752 if tracked and tracked.startswith('refs/remotes/'): 753 return tracked[13:].partition('/')[::2] 754 755 return None, None 756 757 758def resolve_tracking(remote, branch): 759 """Resolve tracked upstream remote/branch if current branch is tracked.""" 760 tracked_remote, tracked_branch = parse_tracking() 761 # tracked_branch will be empty when tracking a local branch 762 if tracked_branch: 763 if VERBOSE: 764 print('Following tracked %s/%s rather than default %s/%s' % ( 765 tracked_remote, tracked_branch, 766 remote, branch)) 767 return tracked_remote, tracked_branch 768 769 return remote, branch 770 771 772def check_remote(branch, remote, scheme, hostname, port, project, 773 usepushurl=False): 774 """Check that a Gerrit Git remote repo exists, if not, set one.""" 775 776 if usepushurl: 777 push_url = git_config_get_value('remote.%s' % remote, 'pushurl', None) 778 if push_url: 779 return 780 else: 781 has_color = check_color_support() 782 if has_color: 783 color_never = "--color=never" 784 else: 785 color_never = "" 786 787 if remote in run_command("git remote").split("\n"): 788 789 remotes = run_command("git branch -a %s" % color_never).split("\n") 790 for current_remote in remotes: 791 remote_string = "remotes/%s/%s" % (remote, branch) 792 if (current_remote.strip() == remote_string and not UPDATE): 793 return 794 # We have the remote, but aren't set up to fetch. Fix it 795 if VERBOSE: 796 print("Setting up gerrit branch tracking for better rebasing") 797 update_remote(remote) 798 return 799 800 if hostname is False or project is False: 801 # This means there was no .gitreview file 802 printwrap("No '.gitreview' file found in this repository. We don't " 803 "know where your gerrit is.") 804 if usepushurl: 805 printwrap("Please set the push-url on your origin remote to the " 806 "location of your gerrit server and try again") 807 else: 808 printwrap("Please manually create a remote named \"%s\" or " 809 "rename the default one and try again." % remote) 810 sys.exit(1) 811 812 # Gerrit remote not present, try to add it 813 try: 814 add_remote(scheme, hostname, port, project, remote, usepushurl) 815 except Exception: 816 if usepushurl: 817 printwrap("We don't know where your gerrit is. Please manually" 818 " add a push-url to the '%s' remote and try again." 819 % remote) 820 else: 821 printwrap("We don't know where your gerrit is. Please manually" 822 " create a remote named '%s' and try again." % remote) 823 raise 824 825 826def rebase_changes(branch, remote, interactive=True): 827 828 global _orig_head 829 830 remote_branch = "remotes/%s/%s" % (remote, branch) 831 832 if not update_remote(remote): 833 return False 834 835 # since the value of ORIG_HEAD may not be set by rebase as expected 836 # for use in undo_rebase, make sure to save it explicitly 837 cmd = "git rev-parse HEAD" 838 (status, output) = run_command_status(cmd) 839 if status != 0: 840 print("Errors running %s" % cmd) 841 if interactive: 842 print(output) 843 return False 844 _orig_head = output 845 846 cmd = "git show-ref --quiet --verify refs/%s" % remote_branch 847 (status, output) = run_command_status(cmd) 848 if status != 0: 849 printwrap("The branch '%s' does not exist on the given remote '%s'. " 850 "If these changes are intended to start a new branch, " 851 "re-run with the '-R' option enabled." % (branch, remote)) 852 sys.exit(1) 853 854 # Determine git version to set rebase flags below. 855 output = run_command("git version") 856 rebase_flag = "--rebase-merges" 857 if "git version" in output: 858 try: 859 v = output.rsplit(None, 1)[1] 860 gitv = tuple(map(int, v.split('.')[:3])) 861 if gitv < (2, 18, 0): 862 rebase_flag = "--preserve-merges" 863 except Exception: 864 # We tried to determine the version and failed. Use current git 865 # flag as fallback. 866 warn("Could not determine git version. " 867 "Using modern git rebase flags.") 868 869 interactive_flag = interactive and '-i' or '' 870 871 cmd = "git rebase %s %s %s" % \ 872 (rebase_flag, interactive_flag, remote_branch) 873 874 (status, output) = run_command_status(cmd, GIT_EDITOR='true') 875 if status != 0: 876 print("Errors running %s" % cmd) 877 if interactive: 878 print(output) 879 printwrap("It is likely that your change has a merge conflict. " 880 "You may resolve it in the working tree now as " 881 "described above and then run 'git review' again, or " 882 "if you do not want to resolve it yet (note that the " 883 "change can not merge until the conflict is resolved) " 884 "you may run 'git rebase --abort' then 'git review -R' " 885 "to upload the change without rebasing.") 886 return False 887 return True 888 889 890def undo_rebase(): 891 global _orig_head 892 if not _orig_head: 893 return True 894 895 cmd = "git reset --hard %s" % _orig_head 896 (status, output) = run_command_status(cmd) 897 if status != 0: 898 print("Errors running %s" % cmd) 899 print(output) 900 return False 901 return True 902 903 904def get_branch_name(target_branch): 905 global _branch_name 906 if _branch_name is not None: 907 return _branch_name 908 cmd = "git rev-parse --symbolic-full-name --abbrev-ref HEAD" 909 _branch_name = run_command(cmd) 910 if _branch_name == "HEAD": 911 # detached head or no branch found 912 _branch_name = target_branch 913 return _branch_name 914 915 916def assert_one_change(remote, branch, yes, have_hook): 917 if check_use_color_output(): 918 use_color = "--color=always" 919 else: 920 use_color = "--color=never" 921 cmd = ("git log %s --decorate --oneline HEAD --not --remotes=%s" % ( 922 use_color, remote)) 923 (status, output) = run_command_status(cmd) 924 if status != 0: 925 print("Had trouble running %s" % cmd) 926 print(output) 927 sys.exit(1) 928 filtered = filter(None, output.split("\n")) 929 output_lines = sum(1 for s in filtered) 930 if output_lines == 1 and not have_hook: 931 printwrap("Your change was committed before the commit hook was " 932 "installed. Amending the commit to add a gerrit change id.") 933 run_command("git commit --amend", GIT_EDITOR='true') 934 elif output_lines == 0: 935 printwrap("No changes between HEAD and %s/%s. Submitting for review " 936 "would be pointless." % (remote, branch)) 937 sys.exit(1) 938 elif output_lines > 1: 939 if not yes: 940 printwrap("You are about to submit multiple commits. This is " 941 "expected if you are submitting a commit that is " 942 "dependent on one or more in-review commits, or if you " 943 "are submitting multiple self-contained but dependent " 944 "changes. Otherwise you should consider squashing your " 945 "changes into one commit before submitting (for " 946 "indivisible changes) or submitting from separate " 947 "branches (for independent changes).") 948 print("\nThe outstanding commits are:\n\n%s\n\n" 949 "Do you really want to submit the above commits?" % output) 950 yes_no = do_input("Type 'yes' to confirm, other to cancel: ") 951 if yes_no.lower().strip() != "yes": 952 print("Aborting.") 953 sys.exit(1) 954 955 956def get_topic(target_branch): 957 branch_name = get_branch_name(target_branch) 958 959 branch_parts = branch_name.split("/") 960 if len(branch_parts) >= 3 and branch_parts[0] == "review": 961 # We don't want to set the review number as the topic 962 if branch_parts[2].isdigit(): 963 return 964 965 topic = "/".join(branch_parts[2:]) 966 if VERBOSE: 967 print("Using change number %s for the topic of the change " 968 "submitted" % topic) 969 return topic 970 971 if VERBOSE: 972 print("Using local branch name %s for the topic of the change " 973 "submitted" % branch_name) 974 return branch_name 975 976 977class CannotQueryOpenChangesets(CommandFailed): 978 "Cannot fetch review information from gerrit" 979 EXIT_CODE = 32 980 981 982class CannotParseOpenChangesets(ChangeSetException): 983 "Cannot parse JSON review information from gerrit" 984 EXIT_CODE = 33 985 986 987class Review(dict): 988 _default_fields = ('branch', 'topic', 'subject') 989 990 def __init__(self, data): 991 if 'number' not in data: 992 raise TypeError("<Review> requires 'number' key in data") 993 994 super(Review, self).__init__(data) 995 996 # provide default values for some fields 997 for field in self._default_fields: 998 self[field] = self.get(field, '-') 999 1000 1001class ReviewsPrinter(object): 1002 def __init__(self, with_topic=False): 1003 if with_topic: 1004 self.fields = ('number', 'branch', 'topic', 'subject') 1005 # > is right justify, < is left, field indices for py26 1006 self.fields_format = [ 1007 u"{0:>{1}}", u"{2:>{3}}", u"{4:>{5}}", u"{6:<{7}}"] 1008 else: 1009 self.fields = ('number', 'branch', 'subject') 1010 # > is right justify, < is left, field indices for py26 1011 self.fields_format = [u"{0:>{1}}", u"{2:>{3}}", u"{4:<{5}}"] 1012 1013 self.fields_colors = ("", "", "", "") 1014 self.color_reset = "" 1015 if check_use_color_output(): 1016 self.fields_colors = ( 1017 colors.yellow, colors.green, colors.blue, "") 1018 self.color_reset = colors.reset 1019 1020 self.reviews = [] 1021 1022 @property 1023 def fields_width(self): 1024 return [ 1025 max(len(str(review[field])) for review in self.reviews) 1026 for field in self.fields[:-1] 1027 ] + [1] 1028 1029 def _get_field_format_str(self, field): 1030 index = self.fields.index(field) 1031 return ( 1032 self.fields_colors[index] + 1033 self.fields_format[index] + 1034 self.color_reset 1035 ) 1036 1037 def add_review(self, review): 1038 self.reviews.append(review) 1039 1040 def _get_fields_format_str(self): 1041 return " ".join([ 1042 self._get_field_format_str(field) 1043 for field in self.fields]) 1044 1045 def print_review(self, review): 1046 fields_format_str = self._get_fields_format_str() 1047 1048 formatted_fields = [] 1049 for field, width in zip(self.fields, self.fields_width): 1050 formatted_fields.extend(( 1051 review[field], width 1052 )) 1053 1054 print(fields_format_str.format(*formatted_fields)) 1055 1056 def do_print(self, reviews): 1057 1058 total_reviews = len(reviews) 1059 1060 for review in reviews: 1061 self.print_review(review) 1062 1063 print("Found %d items for review" % total_reviews) 1064 1065 1066def list_reviews(remote, project, with_topic=False): 1067 remote_url = get_remote_url(remote) 1068 1069 reviews = [] 1070 for r in query_reviews(remote_url, 1071 project=project, 1072 exception=CannotQueryOpenChangesets, 1073 parse_exc=CannotParseOpenChangesets): 1074 reviews.append(Review(r)) 1075 1076 if not reviews: 1077 print("No pending reviews") 1078 return 1079 1080 printer = ReviewsPrinter(with_topic=with_topic) 1081 for review in reviews: 1082 printer.add_review(review) 1083 1084 printer.do_print(reviews) 1085 return 0 1086 1087 1088class CannotQueryPatchSet(CommandFailed): 1089 "Cannot query patchset information" 1090 EXIT_CODE = 34 1091 1092 1093class ReviewInformationNotFound(ChangeSetException): 1094 "Could not fetch review information for change %s" 1095 EXIT_CODE = 35 1096 1097 1098class ReviewNotFound(ChangeSetException): 1099 "Gerrit review %s not found" 1100 EXIT_CODE = 36 1101 1102 1103class PatchSetGitFetchFailed(CommandFailed): 1104 """Cannot fetch patchset contents 1105 1106Does specified change number belong to this project? 1107""" 1108 EXIT_CODE = 37 1109 1110 1111class PatchSetNotFound(ChangeSetException): 1112 "Review patchset %s not found" 1113 EXIT_CODE = 38 1114 1115 1116class GerritConnectionException(GitReviewException): 1117 """Problem to establish connection to gerrit.""" 1118 EXIT_CODE = 40 1119 1120 1121class CheckoutNewBranchFailed(CommandFailed): 1122 "Cannot checkout to new branch" 1123 EXIT_CODE = 64 1124 1125 1126class CheckoutExistingBranchFailed(CommandFailed): 1127 "Cannot checkout existing branch" 1128 EXIT_CODE = 65 1129 1130 1131class ResetHardFailed(CommandFailed): 1132 "Failed to hard reset downloaded branch" 1133 EXIT_CODE = 66 1134 1135 1136class SetUpstreamBranchFailed(CommandFailed): 1137 "Cannot set upstream to remote branch" 1138 EXIT_CODE = 67 1139 1140 1141class SymbolicRefFailed(CommandFailed): 1142 "Cannot find symbolic reference" 1143 EXIT_CODE = 68 1144 1145 1146class ForEachRefFailed(CommandFailed): 1147 "Cannot process symbolic reference" 1148 EXIT_CODE = 69 1149 1150 1151class BranchTrackingMismatch(GitReviewException): 1152 "Branch exists but is tracking unexpected branch" 1153 EXIT_CODE = 70 1154 1155 1156def fetch_review(review, masterbranch, remote, project): 1157 remote_url = get_remote_url(remote) 1158 1159 review_arg = review 1160 review, patchset_number = parse_review_number(review) 1161 current_patch_set = patchset_number is None 1162 1163 review_infos = query_reviews(remote_url, 1164 project=project, 1165 change=review, 1166 current_patch_set=current_patch_set, 1167 exception=CannotQueryPatchSet, 1168 parse_exc=ReviewInformationNotFound) 1169 1170 if not len(review_infos): 1171 raise ReviewInformationNotFound(review) 1172 for info in review_infos: 1173 if 'branch' in info and info['branch'] == masterbranch: 1174 if VERBOSE: 1175 print('Using review info from branch %s' % info['branch']) 1176 review_info = info 1177 break 1178 else: 1179 review_info = review_infos[0] 1180 if VERBOSE and 'branch' in review_info: 1181 print('Using default branch %s' % review_info['branch']) 1182 1183 try: 1184 if patchset_number is None: 1185 refspec = review_info['currentPatchSet']['ref'] 1186 else: 1187 refspec = [ps for ps in review_info['patchSets'] 1188 if int(ps['number']) == int(patchset_number)][0]['ref'] 1189 except IndexError: 1190 raise PatchSetNotFound(review_arg) 1191 except KeyError: 1192 raise ReviewNotFound(review) 1193 1194 try: 1195 topic = review_info['topic'] 1196 if topic == masterbranch: 1197 topic = review 1198 except KeyError: 1199 topic = review 1200 try: 1201 author = re.sub('\W+', '_', review_info['owner']['name']).lower() 1202 except KeyError: 1203 author = 'unknown' 1204 remote_branch = review_info['branch'] 1205 1206 if patchset_number is None: 1207 branch_name = "review/%s/%s" % (author, topic) 1208 else: 1209 branch_name = "review/%s/%s-patch%s" % (author, topic, patchset_number) 1210 1211 print("Downloading %s from gerrit" % refspec) 1212 run_command_exc(PatchSetGitFetchFailed, 1213 "git", "fetch", remote_url, refspec) 1214 return branch_name, remote_branch 1215 1216 1217def checkout_review(branch_name, remote, remote_branch): 1218 """Checkout a newly fetched (FETCH_HEAD) change 1219 into a branch 1220 """ 1221 1222 try: 1223 run_command_exc(CheckoutNewBranchFailed, 1224 "git", "checkout", "-b", 1225 branch_name, "FETCH_HEAD") 1226 # --set-upstream-to is supported starting in git 1.8 1227 if remote is not None: 1228 run_command_exc(SetUpstreamBranchFailed, 1229 "git", "branch", "--set-upstream-to", 1230 '%s/%s' % (remote, remote_branch), 1231 branch_name) 1232 1233 except CheckoutNewBranchFailed as e: 1234 if re.search("already exists\.?", e.output): 1235 print("Branch %s already exists - reusing" % branch_name) 1236 track_remote, track_branch = parse_tracking( 1237 ref='refs/heads/' + branch_name) 1238 if track_remote and not (track_remote == remote and 1239 track_branch == remote_branch): 1240 print("Branch %s incorrectly tracking %s/%s instead of %s/%s" 1241 % (branch_name, 1242 track_remote, track_branch, 1243 remote, remote_branch)) 1244 raise BranchTrackingMismatch 1245 run_command_exc(CheckoutExistingBranchFailed, 1246 "git", "checkout", branch_name) 1247 run_command_exc(ResetHardFailed, 1248 "git", "reset", "--hard", "FETCH_HEAD") 1249 else: 1250 raise 1251 1252 print("Switched to branch \"%s\"" % branch_name) 1253 1254 1255class PatchSetGitCherrypickFailed(CommandFailed): 1256 "There was a problem applying changeset contents to the current branch." 1257 EXIT_CODE = 69 1258 1259 1260def cherrypick_review(option=None): 1261 cmd = ["git", "cherry-pick"] 1262 if option: 1263 cmd.append(option) 1264 cmd.append("FETCH_HEAD") 1265 print(run_command_exc(PatchSetGitCherrypickFailed, *cmd)) 1266 1267 1268class CheckoutBackExistingBranchFailed(CommandFailed): 1269 "Cannot switch back to existing branch" 1270 EXIT_CODE = 67 1271 1272 1273class DeleteBranchFailed(CommandFailed): 1274 "Failed to delete branch" 1275 EXIT_CODE = 68 1276 1277 1278class InvalidPatchsetsToCompare(GitReviewException): 1279 def __init__(self, patchsetA, patchsetB): 1280 Exception.__init__( 1281 self, 1282 "Invalid patchsets for comparison specified (old=%s,new=%s)" % ( 1283 patchsetA, 1284 patchsetB)) 1285 EXIT_CODE = 39 1286 1287 1288def compare_review(review_spec, branch, remote, project, rebase=False): 1289 new_ps = None # none means latest 1290 1291 if '-' in review_spec: 1292 review_spec, new_ps = review_spec.split('-') 1293 review, old_ps = parse_review_number(review_spec) 1294 1295 if old_ps is None or old_ps == new_ps: 1296 raise InvalidPatchsetsToCompare(old_ps, new_ps) 1297 1298 old_review = build_review_number(review, old_ps) 1299 new_review = build_review_number(review, new_ps) 1300 1301 old_branch, _ = fetch_review(old_review, branch, remote, project) 1302 checkout_review(old_branch, None, None) 1303 1304 if rebase: 1305 print('Rebasing %s' % old_branch) 1306 rebase = rebase_changes(branch, remote, False) 1307 if not rebase: 1308 print('Skipping rebase because of conflicts') 1309 run_command_exc(CommandFailed, 'git', 'rebase', '--abort') 1310 1311 new_branch, remote_branch = fetch_review( 1312 new_review, 1313 branch, 1314 remote, 1315 project) 1316 checkout_review(new_branch, remote, remote_branch) 1317 1318 if rebase: 1319 print('Rebasing also %s' % new_branch) 1320 if not rebase_changes(branch, remote, False): 1321 print("Rebasing of the new branch failed, " 1322 "diff can be messed up (use -R to not rebase at all)!") 1323 run_command_exc(CommandFailed, 'git', 'rebase', '--abort') 1324 1325 subprocess.check_call(['git', 'diff', old_branch]) 1326 1327 1328def finish_branch(target_branch): 1329 local_branch = get_branch_name(target_branch) 1330 if VERBOSE: 1331 print("Switching back to '%s' and deleting '%s'" % (target_branch, 1332 local_branch)) 1333 run_command_exc(CheckoutBackExistingBranchFailed, 1334 "git", "checkout", target_branch) 1335 print("Switched to branch '%s'" % target_branch) 1336 1337 run_command_exc(DeleteBranchFailed, 1338 "git", "branch", "-D", local_branch) 1339 print("Deleted branch '%s'" % local_branch) 1340 1341 1342def convert_bool(one_or_zero): 1343 "Return a bool on a one or zero string." 1344 return str(one_or_zero) in ["1", "true", "True"] 1345 1346 1347class MalformedInput(GitReviewException): 1348 EXIT_CODE = 3 1349 1350 1351def assert_valid_reviewers(reviewers): 1352 """Ensure no whitespace is found in reviewer names, as it will result 1353 in an invalid refspec. 1354 """ 1355 for reviewer in reviewers: 1356 if re.search(r'\s', reviewer): 1357 raise MalformedInput( 1358 "Whitespace not allowed in reviewer: '%s'" % reviewer) 1359 1360 1361class _DownloadFlag(argparse.Action): 1362 """Special action for the various forms of downloading reviews. 1363 1364 Additional option parsing: store value in 'dest', but 1365 at the same time set one of the flag options to True 1366 """ 1367 def __call__(self, parser, namespace, value, option_string=None): 1368 url = urlparse(value) 1369 # Turn URLs into change ids: 1370 # https://review.openstack.org/423436 1371 # and 1372 # https://review.openstack.org/423436/ 1373 # and 1374 # https://review.openstack.org/#/c/423436 1375 # and 1376 # https://review.openstack.org/c/<project>/+/423436 1377 # become 1378 # "423436" 1379 # while 1380 # https://review.openstack.org/423436/1 1381 # and 1382 # https://review.openstack.org/#/c/423436/1 1383 # and 1384 # https://review.openstack.org/c/<project>/+/423436/1 1385 # become 1386 # "423436,1". 1387 # 1388 # If there is a #, the rest of the path is stored in the 1389 # "fragment", otherwise that will be empty. 1390 base = url.fragment or url.path 1391 parts = base.rstrip('/').lstrip('/c').split('/') 1392 # PolyGerrit places the change after a '+' symbol in the url 1393 try: 1394 parts = parts[parts.index('+') + 1:] 1395 except ValueError: 1396 pass 1397 change = parts[0] 1398 if len(parts) > 1: 1399 change = '%s,%s' % (change, parts[1]) 1400 setattr(namespace, self.dest, change) 1401 setattr(namespace, self.const, True) 1402 1403 1404def _main(): 1405 usage = "git review [OPTIONS] ... [BRANCH]" 1406 1407 parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT) 1408 1409 topic_arg_group = parser.add_mutually_exclusive_group() 1410 topic_arg_group.add_argument("-t", "--topic", dest="topic", 1411 help="Topic to submit branch to") 1412 topic_arg_group.add_argument("-T", "--no-topic", dest="notopic", 1413 action="store_true", 1414 help="No topic except if explicitly provided") 1415 1416 parser.add_argument("--reviewers", nargs="+", 1417 help="Add reviewers to uploaded patch sets.") 1418 parser.add_argument("-D", "--draft", dest="draft", action="store_true", 1419 help="Submit review as a draft") 1420 parser.add_argument("-n", "--dry-run", dest="dry", action="store_true", 1421 help="Don't actually submit the branch for review") 1422 parser.add_argument("-i", "--new-changeid", dest="regenerate", 1423 action="store_true", 1424 help="Regenerate Change-id before submitting") 1425 parser.add_argument("-r", "--remote", dest="remote", 1426 help="git remote to use for gerrit") 1427 parser.add_argument("--use-pushurl", dest="usepushurl", 1428 action="store_true", 1429 help="Use remote push-url logic instead of separate" 1430 " remotes") 1431 1432 rebase_group = parser.add_mutually_exclusive_group() 1433 rebase_group.add_argument("-R", "--no-rebase", dest="rebase", 1434 action="store_false", 1435 help="Don't rebase changes before submitting.") 1436 rebase_group.add_argument("-F", "--force-rebase", dest="force_rebase", 1437 action="store_true", 1438 help="Force rebase even when not needed.") 1439 1440 track_group = parser.add_mutually_exclusive_group() 1441 track_group.add_argument("--track", dest="track", 1442 action="store_true", 1443 help="Use tracked branch as default.") 1444 track_group.add_argument("--no-track", dest="track", 1445 action="store_false", 1446 help="Ignore tracked branch.") 1447 1448 fetch = parser.add_mutually_exclusive_group() 1449 fetch.set_defaults(download=False, compare=False, cherrypickcommit=False, 1450 cherrypickindicate=False, cherrypickonly=False) 1451 fetch.add_argument("-d", "--download", dest="changeidentifier", 1452 action=_DownloadFlag, metavar="CHANGE[,PS]", 1453 const="download", 1454 help="Download the contents of an existing gerrit " 1455 "review into a branch. Include the patchset " 1456 "number to download a specific version of the " 1457 "change. The default is to take the most recent " 1458 "version.") 1459 fetch.add_argument("-x", "--cherrypick", dest="changeidentifier", 1460 action=_DownloadFlag, metavar="CHANGE", 1461 const="cherrypickcommit", 1462 help="Apply the contents of an existing gerrit " 1463 "review onto the current branch and commit " 1464 "(cherry pick; not recommended in most " 1465 "situations)") 1466 fetch.add_argument("-X", "--cherrypickindicate", dest="changeidentifier", 1467 action=_DownloadFlag, metavar="CHANGE", 1468 const="cherrypickindicate", 1469 help="Apply the contents of an existing gerrit " 1470 "review onto the current branch and commit, " 1471 "indicating its origin") 1472 fetch.add_argument("-N", "--cherrypickonly", dest="changeidentifier", 1473 action=_DownloadFlag, metavar="CHANGE", 1474 const="cherrypickonly", 1475 help="Apply the contents of an existing gerrit " 1476 "review to the working directory and prepare " 1477 "for commit") 1478 fetch.add_argument("-m", "--compare", dest="changeidentifier", 1479 action=_DownloadFlag, metavar="CHANGE,PS[-NEW_PS]", 1480 const="compare", 1481 help="Download specified and latest (or NEW_PS) " 1482 "patchsets of an existing gerrit review into " 1483 "a branches, rebase on master " 1484 "(skipped on conflicts or when -R is specified) " 1485 "and show their differences") 1486 1487 parser.add_argument("-u", "--update", dest="update", action="store_true", 1488 help="Force updates from remote locations") 1489 parser.add_argument("-s", "--setup", dest="setup", action="store_true", 1490 help="Just run the repo setup commands but don't " 1491 "submit anything") 1492 parser.add_argument("-f", "--finish", dest="finish", action="store_true", 1493 help="Close down this branch and switch back to " 1494 "master on successful submission") 1495 parser.add_argument("-l", "--list", dest="list", action="count", 1496 help="List available reviews for the current project, " 1497 "if passed more than once, will show more information") 1498 parser.add_argument("-y", "--yes", dest="yes", action="store_true", 1499 help="Indicate that you do, in fact, understand if " 1500 "you are submitting more than one patch") 1501 parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", 1502 help="Output more information about what's going on") 1503 1504 wip_group = parser.add_mutually_exclusive_group() 1505 wip_group.add_argument("-w", "--work-in-progress", dest="wip", 1506 action="store_true", 1507 help="Send patch as work in progress for Gerrit " 1508 "versions >= 2.15") 1509 wip_group.add_argument("-W", "--ready", dest="ready", action="store_true", 1510 help="Send patch that is already work in progress" 1511 " as ready for review. Gerrit versions >=" 1512 " 2.15") 1513 1514 private_group = parser.add_mutually_exclusive_group() 1515 private_group.add_argument("-p", "--private", dest="private", 1516 action="store_true", 1517 help="Send patch as a private patch ready for " 1518 "review. Gerrit versions >= 2.15") 1519 private_group.add_argument("-P", "--remove-private", dest="remove_private", 1520 action="store_true", 1521 help="Send patch which already in private state" 1522 " to normal patch."" Gerrit versions >= " 1523 "2.15") 1524 1525 parser.add_argument("--no-custom-script", dest="custom_script", 1526 action="store_false", default=True, 1527 help="Do not run custom scripts.") 1528 parser.add_argument("--color", dest="color", metavar="<when>", 1529 nargs="?", choices=["always", "never", "auto"], 1530 help="Show color output. --color (without [<when>]) " 1531 "is the same as --color=always. <when> can be " 1532 "one of %(choices)s. Behaviour can also be " 1533 "controlled by the color.ui and color.review " 1534 "configuration settings.") 1535 parser.add_argument("--no-color", dest="color", action="store_const", 1536 const="never", 1537 help="Turn off colored output. Can be used to " 1538 "override configuration options. Same as " 1539 "setting --color=never.") 1540 parser.add_argument("--license", dest="license", action="store_true", 1541 help="Print the license and exit") 1542 parser.add_argument("--version", action="version", 1543 version='%s version %s' % 1544 (os.path.split(sys.argv[0])[-1], get_version())) 1545 parser.add_argument("branch", nargs="?") 1546 1547 parser.set_defaults(dry=False, 1548 draft=False, 1549 verbose=False, 1550 update=False, 1551 setup=False, 1552 list=False, 1553 yes=False, 1554 wip=False, 1555 ready=False, 1556 private=False, 1557 remove_private=False) 1558 1559 try: 1560 (top_dir, git_dir) = git_directories() 1561 except GitDirectoriesException as _no_git_dir: 1562 no_git_dir = _no_git_dir 1563 else: 1564 no_git_dir = False 1565 config = Config(os.path.join(top_dir, ".gitreview")) 1566 parser.set_defaults(rebase=convert_bool(config['rebase']), 1567 track=convert_bool(config['track']), 1568 remote=None, 1569 usepushurl=convert_bool(config['usepushurl'])) 1570 options = parser.parse_args() 1571 1572 if options.license: 1573 print(COPYRIGHT) 1574 sys.exit(0) 1575 1576 if no_git_dir: 1577 raise no_git_dir 1578 1579 if options.branch is None: 1580 branch = config['branch'] 1581 else: 1582 # explicitly-specified branch on command line overrides options.track 1583 branch = options.branch 1584 options.track = False 1585 1586 global VERBOSE 1587 global UPDATE 1588 VERBOSE = options.verbose 1589 UPDATE = options.update 1590 remote = options.remote 1591 if not remote: 1592 if options.usepushurl: 1593 remote = 'origin' 1594 else: 1595 remote = config['remote'] 1596 yes = options.yes 1597 status = 0 1598 1599 if options.track: 1600 remote, branch = resolve_tracking(remote, branch) 1601 1602 check_remote(branch, remote, config['scheme'], 1603 config['hostname'], config['port'], config['project'], 1604 usepushurl=options.usepushurl) 1605 1606 if options.color: 1607 set_color_output(options.color) 1608 1609 if options.changeidentifier: 1610 if options.compare: 1611 compare_review(options.changeidentifier, 1612 branch, remote, config['project'], 1613 options.rebase) 1614 return 1615 local_branch, remote_branch = fetch_review(options.changeidentifier, 1616 branch, remote, 1617 config['project']) 1618 if options.download: 1619 checkout_review(local_branch, remote, remote_branch) 1620 else: 1621 if options.cherrypickcommit: 1622 cherrypick_review() 1623 elif options.cherrypickonly: 1624 cherrypick_review("-n") 1625 if options.cherrypickindicate: 1626 cherrypick_review("-x") 1627 return 1628 elif options.list: 1629 with_topic = options.list > 1 1630 list_reviews(remote, config['project'], with_topic=with_topic) 1631 return 1632 1633 if options.custom_script: 1634 run_custom_script("pre") 1635 1636 hook_file = os.path.join(git_dir, "hooks", "commit-msg") 1637 have_hook = os.path.exists(hook_file) and os.access(hook_file, os.X_OK) 1638 1639 if not have_hook: 1640 set_hooks_commit_msg(remote, hook_file) 1641 1642 if options.setup: 1643 if options.finish and not options.dry: 1644 finish_branch(branch) 1645 return 1646 1647 if options.rebase or options.force_rebase: 1648 if not rebase_changes(branch, remote): 1649 sys.exit(1) 1650 if not options.force_rebase and not undo_rebase(): 1651 sys.exit(1) 1652 assert_one_change(remote, branch, yes, have_hook) 1653 1654 ref = "for" 1655 1656 if options.draft: 1657 ref = "drafts" 1658 if options.custom_script: 1659 run_custom_script("draft") 1660 1661 cmd = "git push %s HEAD:refs/%s/%s" % (remote, ref, branch) 1662 push_options = [] 1663 if options.topic is not None: 1664 topic = options.topic 1665 else: 1666 topic = None if options.notopic else get_topic(branch) 1667 1668 if topic and topic != branch: 1669 push_options.append("topic=%s" % topic) 1670 1671 if options.reviewers: 1672 assert_valid_reviewers(options.reviewers) 1673 push_options += ["r=%s" % r for r in options.reviewers] 1674 1675 if options.regenerate: 1676 print("Amending the commit to regenerate the change id\n") 1677 regenerate_cmd = "git commit --amend" 1678 if options.dry: 1679 print("\tGIT_EDITOR=\"sed -i -e '/^Change-Id:/d'\" %s\n" % 1680 regenerate_cmd) 1681 else: 1682 run_command(regenerate_cmd, 1683 GIT_EDITOR="sed -i -e " 1684 "'/^Change-Id:/d'") 1685 1686 if options.wip: 1687 push_options.append('wip') 1688 1689 if options.ready: 1690 push_options.append('ready') 1691 1692 if options.private: 1693 push_options.append('private') 1694 1695 if options.remove_private: 1696 push_options.append('remove-private') 1697 1698 if push_options: 1699 cmd += "%" + ",".join(push_options) 1700 if options.dry: 1701 print("Please use the following command " 1702 "to send your commits to review:\n") 1703 print("\t%s\n" % cmd) 1704 else: 1705 (status, output) = run_command_status(cmd) 1706 print(output) 1707 1708 if options.finish and not options.dry and status == 0: 1709 finish_branch(branch) 1710 return 1711 1712 if options.custom_script: 1713 run_custom_script("post") 1714 sys.exit(status) 1715 1716 1717def main(): 1718 # workaround for avoiding UnicodeEncodeError on print() with older python 1719 if sys.version_info[0] < 3: 1720 # without reload print would fail even if sys.stdin.encoding 1721 # would report utf-8 1722 # see: https://stackoverflow.com/a/23847316/99834 1723 stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr 1724 reload(sys) 1725 sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr 1726 sys.setdefaultencoding(os.environ.get('PYTHONIOENCODING', 'utf-8')) 1727 1728 try: 1729 _main() 1730 except GitReviewException as e: 1731 print(e) 1732 sys.exit(e.EXIT_CODE) 1733 1734 1735if __name__ == "__main__": 1736 main() 1737