-#!/usr/bin/env python\r
-from __future__ import print_function\r
-\r
-COPYRIGHT = """\\r
-Copyright (C) 2011-2012 OpenStack LLC.\r
-\r
-Licensed under the Apache License, Version 2.0 (the "License");\r
-you may not use this file except in compliance with the License.\r
-You may obtain a copy of the License at\r
-\r
- http://www.apache.org/licenses/LICENSE-2.0\r
-\r
-Unless required by applicable law or agreed to in writing, software\r
-distributed under the License is distributed on an "AS IS" BASIS,\r
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r
-implied.\r
-\r
-See the License for the specific language governing permissions and\r
-limitations under the License."""\r
-\r
-import datetime\r
-import json\r
-import os\r
-import re\r
-import shlex\r
-import subprocess\r
-import sys\r
-import time\r
-\r
-if sys.version < '3':\r
- import ConfigParser\r
- import urllib\r
- import urlparse\r
- urlopen = urllib.urlopen\r
- urlparse = urlparse.urlparse\r
- do_input = raw_input\r
-else:\r
- import configparser as ConfigParser\r
- import urllib.parse\r
- import urllib.request\r
- urlopen = urllib.request.urlopen\r
- urlparse = urllib.parse.urlparse\r
- do_input = input\r
-\r
-from distutils import version as du_version\r
-\r
-version = "1.22"\r
-\r
-VERBOSE = False\r
-UPDATE = False\r
-CONFIGDIR = os.path.expanduser("~/.config/git-review")\r
-GLOBAL_CONFIG = "/etc/git-review/git-review.conf"\r
-USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf")\r
-PYPI_URL = "http://pypi.python.org/pypi/git-review/json"\r
-PYPI_CACHE_TIME = 60 * 60 * 24 # 24 hours\r
-DEFAULTS = dict(hostname='gerrit.lud.stericsson.com', port='29418', project=False,\r
- defaultbranch='master', defaultremote="gerrit",\r
- defaultrebase="0")\r
-\r
-_branch_name = None\r
-_has_color = None\r
-_no_color_support = False\r
-\r
-\r
-class colors:\r
- yellow = '\033[33m'\r
- green = '\033[92m'\r
- reset = '\033[0m'\r
-\r
-\r
-class GitReviewException(Exception):\r
- pass\r
-\r
-\r
-class CommandFailed(GitReviewException):\r
-\r
- def __init__(self, *args):\r
- Exception.__init__(self, *args)\r
- (self.rc, self.output, self.argv, self.envp) = args\r
- self.quickmsg = dict([\r
- ("argv", " ".join(self.argv)),\r
- ("rc", self.rc),\r
- ("output", self.output)])\r
-\r
- def __str__(self):\r
- return self.__doc__ + """\r
-The following command failed with exit code %(rc)d\r
- "%(argv)s"\r
------------------------\r
-%(output)s\r
------------------------""" % self.quickmsg\r
-\r
-\r
-class ChangeSetException(GitReviewException):\r
-\r
- def __init__(self, e):\r
- GitReviewException.__init__(self)\r
- self.e = str(e)\r
-\r
- def __str__(self):\r
- return self.__doc__ % self.e\r
-\r
-\r
-def parse_review_number(review):\r
- parts = review.split(',')\r
- if len(parts) < 2:\r
- parts.append(None)\r
- return parts\r
-\r
-\r
-def build_review_number(review, patchset):\r
- if patchset is not None:\r
- return '%s,%s' % (review, patchset)\r
- return review\r
-\r
-\r
-def run_command_status(*argv, **env):\r
- if VERBOSE:\r
- print(datetime.datetime.now(), "Running:", " ".join(argv))\r
- if len(argv) == 1:\r
- argv = shlex.split(str(argv[0]))\r
- newenv = os.environ\r
- newenv.update(env)\r
- p = subprocess.Popen(argv, stdout=subprocess.PIPE,\r
- stderr=subprocess.STDOUT, env=newenv)\r
- (out, nothing) = p.communicate()\r
- out = out.decode('utf-8')\r
- return (p.returncode, out.strip())\r
-\r
-\r
-def run_command(*argv, **env):\r
- (rc, output) = run_command_status(*argv, **env)\r
- return output\r
-\r
-\r
-def run_command_exc(klazz, *argv, **env):\r
- """Run command *argv, on failure raise 'klazz\r
-\r
- klass should be derived from CommandFailed\r
- """\r
- (rc, output) = run_command_status(*argv, **env)\r
- if rc != 0:\r
- raise klazz(rc, output, argv, env)\r
- return output\r
-\r
-\r
-def update_latest_version(version_file_path):\r
- """Cache the latest version of git-review for the upgrade check."""\r
-\r
- if not os.path.exists(CONFIGDIR):\r
- os.makedirs(CONFIGDIR)\r
-\r
- if os.path.exists(version_file_path) and not UPDATE:\r
- if (time.time() - os.path.getmtime(version_file_path)) < 28800:\r
- return\r
-\r
- latest_version = version\r
- try:\r
- latest_version = json.load(urlopen(PYPI_URL))['info']['version']\r
- except Exception:\r
- pass\r
-\r
- with open(version_file_path, "w") as version_file:\r
- version_file.write(latest_version)\r
-\r
-\r
-def latest_is_newer():\r
- """Check if there is a new version of git-review."""\r
-\r
- # Skip version check if distro package turns it off\r
- if os.path.exists(GLOBAL_CONFIG):\r
- config = dict(check=False)\r
- configParser = ConfigParser.ConfigParser(config)\r
- configParser.read(GLOBAL_CONFIG)\r
- if not configParser.getboolean("updates", "check"):\r
- return False\r
-\r
- version_file_path = os.path.join(CONFIGDIR, "latest-version")\r
- update_latest_version(version_file_path)\r
-\r
- latest_version = None\r
- with open(version_file_path, "r") as version_file:\r
- latest_version = du_version.StrictVersion(version_file.read())\r
- if latest_version > du_version.StrictVersion(version):\r
- return True\r
- return False\r
-\r
-\r
-def git_directories():\r
- """Determine (absolute git work directory path, .git subdirectory path)."""\r
- cmd = ("git", "rev-parse", "--show-toplevel", "--git-dir")\r
- out = run_command_exc(GitDirectoriesException, *cmd)\r
- try:\r
- return out.split()\r
- except ValueError:\r
- raise GitDirectoriesException(0, out, cmd, {})\r
-\r
-\r
-class GitDirectoriesException(CommandFailed):\r
- "Cannot determine where .git directory is."\r
- EXIT_CODE = 70\r
-\r
-\r
-class CustomScriptException(CommandFailed):\r
- """Custom script execution failed."""\r
- EXIT_CODE = 71\r
-\r
-\r
-def run_custom_script(action):\r
- """Get status and output of .git/hooks/$action-review or/and\r
- ~/.config/hooks/$action-review if existing.\r
- """\r
- returns = []\r
- script_file = "%s-review" % (action)\r
- (top_dir, git_dir) = git_directories()\r
- paths = [os.path.join(CONFIGDIR, "hooks", script_file),\r
- os.path.join(git_dir, "hooks", script_file)]\r
- for fpath in paths:\r
- if os.path.isfile(fpath) and os.access(fpath, os.X_OK):\r
- status, output = run_command_status(fpath)\r
- returns.append((status, output, fpath))\r
-\r
- for (status, output, path) in returns:\r
- if status is not None and status != 0:\r
- raise CustomScriptException(status, output, [path], {})\r
- elif output and VERBOSE:\r
- print("script %s output is:" % (path))\r
- print(output)\r
-\r
-\r
-def git_config_get_value(section, option, default=None):\r
- try:\r
- return run_command_exc(GitConfigException,\r
- "git", "config",\r
- "--get",\r
- "%s.%s" % (section, option)).strip()\r
- except GitConfigException as exc:\r
- if exc.rc == 1:\r
- return default\r
- raise\r
-\r
-\r
-class GitConfigException(CommandFailed):\r
- """Git config value retrieval failed."""\r
- EXIT_CODE = 128\r
-\r
-\r
-class CannotInstallHook(CommandFailed):\r
- "Problems encountered installing commit-msg hook"\r
- EXIT_CODE = 2\r
-\r
-\r
-def set_hooks_commit_msg(remote, target_file):\r
- """Install the commit message hook if needed."""\r
-\r
- # Create the hooks directory if it's not there already\r
- hooks_dir = os.path.dirname(target_file)\r
- if not os.path.isdir(hooks_dir):\r
- os.mkdir(hooks_dir)\r
-\r
- (hostname, username, port, project_name) = \\r
- parse_git_show(remote, "Push")\r
-\r
- if not os.path.exists(target_file) or UPDATE:\r
- if VERBOSE:\r
- print("Fetching commit hook from: scp://%s:%s" % (hostname, port))\r
- if port is not None:\r
- port = "-P %s" % port\r
- else:\r
- port = ""\r
- if username is None:\r
- userhost = hostname\r
- else:\r
- userhost = "%s@%s" % (username, hostname)\r
- run_command_exc(\r
- CannotInstallHook,\r
- "scp", port,\r
- userhost + ":hooks/commit-msg",\r
- target_file)\r
-\r
- if not os.access(target_file, os.X_OK):\r
- os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC)\r
-\r
-\r
-def test_remote(username, hostname, port, project):\r
- """Tests that a possible gerrit remote works."""\r
-\r
- if port is not None:\r
- port = "-p %s" % port\r
- else:\r
- port = ""\r
- if username is None:\r
- userhost = hostname\r
- else:\r
- userhost = "%s@%s" % (username, hostname)\r
-\r
- (status, ssh_output) = run_command_status(\r
- "ssh", "-x", port, userhost,\r
- "gerrit", "ls-projects")\r
-\r
- if status == 0:\r
- if VERBOSE:\r
- print("%s@%s:%s worked." % (username, hostname, port))\r
- return True\r
- else:\r
- if VERBOSE:\r
- print("%s@%s:%s did not work." % (username, hostname, port))\r
- return False\r
-\r
-\r
-def make_remote_url(username, hostname, port, project):\r
- """Builds a gerrit remote URL."""\r
- if username is None:\r
- return "ssh://%s:%s/%s" % (hostname, port, project)\r
- else:\r
- return "ssh://%s@%s:%s/%s" % (username, hostname, port, project)\r
-\r
-\r
-def add_remote(hostname, port, project, remote):\r
- """Adds a gerrit remote."""\r
- asked_for_username = False\r
-\r
- username = os.getenv("USERNAME")\r
- if not username:\r
- username = git_config_get_value("gitreview", "username")\r
- if not username:\r
- username = os.getenv("USER")\r
- if port is None:\r
- port = 29418\r
-\r
- remote_url = make_remote_url(username, hostname, port, project)\r
- if VERBOSE:\r
- print("No remote set, testing %s" % remote_url)\r
- if not test_remote(username, hostname, port, project):\r
- print("Could not connect to gerrit.")\r
- username = do_input("Enter your gerrit username: ")\r
- remote_url = make_remote_url(username, hostname, port, project)\r
- print("Trying again with %s" % remote_url)\r
- if not test_remote(username, hostname, port, project):\r
- raise Exception("Could not connect to gerrit at %s" % remote_url)\r
- asked_for_username = True\r
-\r
- print("Creating a git remote called \"%s\" that maps to:" % remote)\r
- print("\t%s" % remote_url)\r
- cmd = "git remote add -f %s %s" % (remote, remote_url)\r
- (status, remote_output) = run_command_status(cmd)\r
-\r
- if status != 0:\r
- raise Exception("Error running %s" % cmd)\r
-\r
- if asked_for_username:\r
- print()\r
- print("This repository is now set up for use with git-review.")\r
- print("You can set the default username for future repositories with:")\r
- print(' git config --global --add gitreview.username "%s"' % username)\r
- print()\r
-\r
-\r
-def parse_git_show(remote, verb):\r
- fetch_url = ""\r
- for line in run_command("git remote show -n %s" % remote).split("\n"):\r
- if line.strip().startswith("%s" % verb):\r
- fetch_url = line.split()[2]\r
-\r
- parsed_url = urlparse(fetch_url)\r
- project_name = parsed_url.path.lstrip("/")\r
- if project_name.endswith(".git"):\r
- project_name = project_name[:-4]\r
-\r
- hostname = parsed_url.netloc\r
- username = None\r
- port = parsed_url.port\r
-\r
- if VERBOSE:\r
- print("Found origin %s URL:" % verb, fetch_url)\r
-\r
- # Workaround bug in urlparse on OSX\r
- if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":\r
- hostname = parsed_url.path[2:].split("/")[0]\r
-\r
- if "@" in hostname:\r
- (username, hostname) = hostname.split("@")\r
- if ":" in hostname:\r
- (hostname, port) = hostname.split(":")\r
-\r
- # Is origin an ssh location? Let's pull more info\r
- if parsed_url.scheme == "ssh" and port is None:\r
- port = 22\r
-\r
- return (hostname, username, str(port), project_name)\r
-\r
-\r
-def check_color_support():\r
- global _has_color\r
- global _no_color_support\r
- if _has_color is None:\r
- test_command = "git log --color=never --oneline HEAD^1..HEAD"\r
- (status, output) = run_command_status(test_command)\r
- if status == 0:\r
- _has_color = True\r
- else:\r
- _has_color = False\r
- \r
- if _no_color_support:\r
- _has_color = False\r
-\r
- return _has_color\r
-\r
-\r
-def get_config(config_file=None):\r
- """Generate the configuration map by starting with some built-in defaults\r
- and then loading GLOBAL_CONFIG, USER_CONFIG, and a repository-specific\r
- .gitreview file, if they exist. In case of conflict, the configuration file\r
- with the narrowest scope wins.\r
- """\r
- config = DEFAULTS.copy()\r
- for filename in (GLOBAL_CONFIG, USER_CONFIG, config_file):\r
- if filename is not None and os.path.exists(filename):\r
- config.update(load_config_file(filename))\r
- return config\r
-\r
-\r
-def load_config_file(config_file):\r
- """Load configuration options from a file."""\r
- configParser = ConfigParser.ConfigParser()\r
- configParser.read(config_file)\r
- options = {\r
- 'hostname': 'host',\r
- 'port': 'port',\r
- 'project': 'project',\r
- 'defaultbranch': 'defaultbranch',\r
- 'defaultremote': 'defaultremote',\r
- 'defaultrebase': 'defaultrebase',\r
- }\r
- config = {}\r
- for config_key, option_name in options.items():\r
- if configParser.has_option('gerrit', option_name):\r
- config[config_key] = configParser.get('gerrit', option_name)\r
- return config\r
-\r
-\r
-def update_remote(remote):\r
- cmd = "git remote update %s" % remote\r
- (status, output) = run_command_status(cmd)\r
- if VERBOSE:\r
- print(output)\r
- if status != 0:\r
- print("Problem running '%s'" % cmd)\r
- if not VERBOSE:\r
- print(output)\r
- return False\r
- return True\r
-\r
-\r
-def check_remote(branch, remote, hostname, port, project):\r
- """Check that a Gerrit Git remote repo exists, if not, set one."""\r
-\r
- has_color = check_color_support()\r
- if has_color:\r
- color_never = "--color=never"\r
- else:\r
- color_never = ""\r
-\r
- if remote in run_command("git remote").split("\n"):\r
-\r
- remotes = run_command("git branch -a %s" % color_never).split("\n")\r
- for current_remote in remotes:\r
- if (current_remote.strip() == "remotes/%s/%s" % (remote, branch)\r
- and not UPDATE):\r
- return\r
- # We have the remote, but aren't set up to fetch. Fix it\r
- if VERBOSE:\r
- print("Setting up gerrit branch tracking for better rebasing")\r
- update_remote(remote)\r
- return\r
-\r
- if hostname is False or port is False or project is False:\r
- # This means there was no .gitreview file\r
- print("No '.gitreview' file found in this repository.")\r
- print("We don't know where your gerrit is. Please manually create ")\r
- print("a remote named \"%s\" and try again." % remote)\r
- sys.exit(1)\r
-\r
- # Gerrit remote not present, try to add it\r
- try:\r
- add_remote(hostname, port, project, remote)\r
- except Exception:\r
- print(sys.exc_info()[2])\r
- print("We don't know where your gerrit is. Please manually create ")\r
- print("a remote named \"%s\" and try again." % remote)\r
- raise\r
-\r
-\r
-def rebase_changes(branch, remote, interactive=True):\r
-\r
- remote_branch = "remotes/%s/%s" % (remote, branch)\r
-\r
- if not update_remote(remote):\r
- return False\r
-\r
- if interactive:\r
- cmd = "git rebase -i %s" % remote_branch\r
- else:\r
- cmd = "git rebase %s" % remote_branch\r
-\r
- (status, output) = run_command_status(cmd, GIT_EDITOR='true')\r
- if status != 0:\r
- print("Errors running %s" % cmd)\r
- if interactive:\r
- print(output)\r
- return False\r
- return True\r
-\r
-\r
-def undo_rebase():\r
- cmd = "git reset --hard ORIG_HEAD"\r
- (status, output) = run_command_status(cmd)\r
- if status != 0:\r
- print("Errors running %s" % cmd)\r
- print(output)\r
- return False\r
- return True\r
-\r
-\r
-def get_branch_name(target_branch):\r
- global _branch_name\r
- if _branch_name is not None:\r
- return _branch_name\r
- _branch_name = None\r
- has_color = check_color_support()\r
- if has_color:\r
- color_never = "--color=never"\r
- else:\r
- color_never = ""\r
- for branch in run_command("git branch %s" % color_never).split("\n"):\r
- if branch.startswith('*'):\r
- _branch_name = branch.split()[1].strip()\r
- if _branch_name == "(no":\r
- _branch_name = target_branch\r
- return _branch_name\r
-\r
-\r
-def assert_one_change(remote, branch, yes, have_hook):\r
- has_color = check_color_support()\r
- if has_color:\r
- color = git_config_get_value("color", "ui")\r
- if color is None:\r
- color = "auto"\r
- else:\r
- color = color.lower()\r
- if (color == "" or color == "true"):\r
- color = "auto"\r
- elif color == "false":\r
- color = "never"\r
- elif color == "auto":\r
- # Python is not a tty, we have to force colors\r
- color = "always"\r
- use_color = "--color=%s" % color\r
- else:\r
- use_color = ""\r
- cmd = "git log %s --decorate --oneline HEAD --not remotes/%s/%s --" % (\r
- use_color, remote, branch)\r
- (status, output) = run_command_status(cmd)\r
- if status != 0:\r
- print("Had trouble running %s" % cmd)\r
- print(output)\r
- sys.exit(1)\r
- output_lines = len(output.split("\n"))\r
- if output_lines == 1 and not have_hook:\r
- print("Your change was committed before the commit hook was installed")\r
- print("Amending the commit to add a gerrit change id")\r
- run_command("git commit --amend", GIT_EDITOR='true')\r
- elif output_lines == 0:\r
- print("No changes between HEAD and %s/%s." % (remote, branch))\r
- print("Submitting for review would be pointless.")\r
- sys.exit(1)\r
- elif output_lines > 1:\r
- if not yes:\r
- print("You have more than one commit"\r
- " that you are about to submit.")\r
- print("The outstanding commits are:\n\n%s\n" % output)\r
- print("Is this really what you meant to do?")\r
- yes_no = do_input("Type 'yes' to confirm: ")\r
- if yes_no.lower().strip() != "yes":\r
- print("Aborting.")\r
- print("Please rebase/squash your changes and try again")\r
- sys.exit(1)\r
-\r
-\r
-def use_topic(why, topic):\r
- """Inform the user about why a particular topic has been selected."""\r
- if VERBOSE:\r
- print(why % ('"%s"' % topic,))\r
- return topic\r
-\r
-\r
-def get_topic(target_branch):\r
-\r
- branch_name = get_branch_name(target_branch)\r
-\r
- branch_parts = branch_name.split("/")\r
- if len(branch_parts) >= 3 and branch_parts[0] == "review":\r
- return use_topic("Using change number %s "\r
- "for the topic of the change submitted",\r
- "/".join(branch_parts[2:]))\r
-\r
- log_output = run_command("git log HEAD^1..HEAD")\r
- bug_re = r'\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)'\r
-\r
- match = re.search(bug_re, log_output)\r
- if match is not None:\r
- return use_topic("Using bug number %s "\r
- "for the topic of the change submitted",\r
- "bug/%s" % match.group(2))\r
-\r
- bp_re = r'\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)'\r
- match = re.search(bp_re, log_output)\r
- if match is not None:\r
- return use_topic("Using blueprint number %s "\r
- "for the topic of the change submitted",\r
- "bp/%s" % match.group(2))\r
-\r
- return use_topic("Using local branch name %s "\r
- "for the topic of the change submitted",\r
- branch_name)\r
-\r
-\r
-class CannotQueryOpenChangesets(CommandFailed):\r
- "Cannot fetch review information from gerrit"\r
- EXIT_CODE = 32\r
-\r
-\r
-class CannotParseOpenChangesets(ChangeSetException):\r
- "Cannot parse JSON review information from gerrit"\r
- EXIT_CODE = 33\r
-\r
-\r
-def list_reviews(remote):\r
-\r
- (hostname, username, port, project_name) = \\r
- parse_git_show(remote, "Push")\r
-\r
- if port is not None:\r
- port = "-p %s" % port\r
- else:\r
- port = ""\r
- if username is None:\r
- userhost = hostname\r
- else:\r
- userhost = "%s@%s" % (username, hostname)\r
-\r
- review_info = None\r
- output = run_command_exc(\r
- CannotQueryOpenChangesets,\r
- "ssh", "-x", port, userhost,\r
- "gerrit", "query",\r
- "--current-patch-set --format=JSON project:%s status:open reviewer:self" % project_name)\r
-\r
- review_list = []\r
- review_field_width = {}\r
- REVIEW_FIELDS = ('number', 'currentPatchSet', 'branch', 'subject')\r
- FIELDS = range(0, len(REVIEW_FIELDS))\r
- if check_color_support():\r
- review_field_color = (colors.yellow, colors.yellow, colors.green, "")\r
- color_reset = colors.reset\r
- else:\r
- review_field_color = ("", "", "", "")\r
- color_reset = ""\r
- review_field_width = [0, 0, 0, 0]\r
- review_field_format = ["%*s", "%*s", "%*s", "%*s"]\r
- review_field_justify = [+1, +1, +1, -1] # -1 is justify to right\r
-\r
- for line in output.split("\n"):\r
- # Warnings from ssh wind up in this output\r
- if line[0] != "{":\r
- print(line)\r
- continue\r
- try:\r
- review_info = json.loads(line)\r
- except Exception:\r
- if VERBOSE:\r
- print(output)\r
- raise(CannotParseOpenChangesets, sys.exc_info()[1])\r
-\r
- if 'type' in review_info:\r
- break\r
-\r
- tempPS = review_info['currentPatchSet']\r
- appPS = tempPS['approvals']\r
- appValue = '-';\r
- for appLine in appPS:\r
- appBy = appLine['by']\r
- appUser = appBy['username']\r
- if appUser == username:\r
- appValue = appLine['value']\r
- review_info['currentPatchSet'] = tempPS['number'] + ' ' + appValue\r
- \r
- review_list.append([review_info[f] for f in REVIEW_FIELDS])\r
- for i in FIELDS:\r
- review_field_width[i] = max(\r
- review_field_width[i],\r
- len(review_info[REVIEW_FIELDS[i]])\r
- )\r
-\r
- review_field_format = " ".join([\r
- review_field_color[i] +\r
- review_field_format[i] +\r
- color_reset\r
- for i in FIELDS])\r
-\r
- review_field_width = [\r
- review_field_width[i] * review_field_justify[i]\r
- for i in FIELDS]\r
- for review_value in review_list:\r
- # At this point we have review_field_format\r
- # like "%*s %*s %*s" and we need to supply\r
- # (width1, value1, width2, value2, ...) tuple to print\r
- # It's easy to zip() widths with actual values,\r
- # but we need to flatten the resulting\r
- # ((width1, value1), (width2, value2), ...) map.\r
- formatted_fields = []\r
- for (width, value) in zip(review_field_width, review_value):\r
- formatted_fields.extend([width, value])\r
- print(review_field_format % tuple(formatted_fields))\r
- print("Found %d items for review" % review_info['rowCount'])\r
-\r
- return 0\r
-\r
-\r
-class CannotQueryPatchSet(CommandFailed):\r
- "Cannot query patchset information"\r
- EXIT_CODE = 34\r
-\r
-\r
-class ReviewInformationNotFound(ChangeSetException):\r
- "Could not fetch review information for change %s"\r
- EXIT_CODE = 35\r
-\r
-\r
-class ReviewNotFound(ChangeSetException):\r
- "Gerrit review %s not found"\r
- EXIT_CODE = 36\r
-\r
-\r
-class PatchSetGitFetchFailed(CommandFailed):\r
- """Cannot fetch patchset contents\r
-\r
-Does specified change number belong to this project?\r
-"""\r
- EXIT_CODE = 37\r
-\r
-\r
-class PatchSetNotFound(ChangeSetException):\r
- "Review patchset %s not found"\r
- EXIT_CODE = 38\r
-\r
-\r
-class CheckoutNewBranchFailed(CommandFailed):\r
- "Cannot checkout to new branch"\r
- EXIT_CODE = 64\r
-\r
-\r
-class CheckoutExistingBranchFailed(CommandFailed):\r
- "Cannot checkout existing branch"\r
- EXIT_CODE = 65\r
-\r
-\r
-class ResetHardFailed(CommandFailed):\r
- "Failed to hard reset downloaded branch"\r
- EXIT_CODE = 66\r
-\r
-\r
-def fetch_review(review, masterbranch, remote):\r
-\r
- (hostname, username, port, project_name) = \\r
- parse_git_show(remote, "Push")\r
-\r
- if port is not None:\r
- port = "-p %s" % port\r
- else:\r
- port = ""\r
- if username is None:\r
- userhost = hostname\r
- else:\r
- userhost = "%s@%s" % (username, hostname)\r
-\r
- review_arg = review\r
- patchset_opt = '--current-patch-set'\r
-\r
- review, patchset_number = parse_review_number(review)\r
- if patchset_number is not None:\r
- patchset_opt = '--patch-sets'\r
-\r
- review_info = None\r
- output = run_command_exc(\r
- CannotQueryPatchSet,\r
- "ssh", "-x", port, userhost,\r
- "gerrit", "query",\r
- "--format=JSON %s change:%s" % (patchset_opt, review))\r
-\r
- review_jsons = output.split("\n")\r
- found_review = False\r
- for review_json in review_jsons:\r
- try:\r
- review_info = json.loads(review_json)\r
- found_review = True\r
- except Exception:\r
- pass\r
- if found_review:\r
- break\r
- if not found_review:\r
- if VERBOSE:\r
- print(output)\r
- raise ReviewInformationNotFound(review)\r
-\r
- try:\r
- if patchset_number is None:\r
- refspec = review_info['currentPatchSet']['ref']\r
- else:\r
- refspec = [ps for ps\r
- in review_info['patchSets']\r
- if ps['number'] == patchset_number][0]['ref']\r
- except IndexError:\r
- raise PatchSetNotFound(review_arg)\r
- except KeyError:\r
- raise ReviewNotFound(review)\r
-\r
- try:\r
- topic = review_info['topic']\r
- if topic == masterbranch:\r
- topic = review\r
- except KeyError:\r
- topic = review\r
- try:\r
- author = re.sub('\W+', '_', review_info['owner']['name']).lower()\r
- except KeyError:\r
- author = 'unknown'\r
-\r
- if patchset_number is None:\r
- branch_name = "review/%s/%s" % (author, topic)\r
- else:\r
- branch_name = "review/%s/%s-patch%s" % (author, topic, patchset_number)\r
-\r
- print("Downloading %s from gerrit" % refspec)\r
- run_command_exc(PatchSetGitFetchFailed,\r
- "git", "fetch", remote, refspec)\r
- return branch_name\r
-\r
-\r
-def checkout_review(branch_name):\r
- """Checkout a newly fetched (FETCH_HEAD) change\r
- into a branch\r
- """\r
-\r
- try:\r
- run_command_exc(CheckoutNewBranchFailed,\r
- "git", "checkout", "-b",\r
- branch_name, "FETCH_HEAD")\r
-\r
- except CheckoutNewBranchFailed as e:\r
- if re.search("already exists\.?", e.output):\r
- print("Branch already exists - reusing")\r
- run_command_exc(CheckoutExistingBranchFailed,\r
- "git", "checkout", branch_name)\r
- run_command_exc(ResetHardFailed,\r
- "git", "reset", "--hard", "FETCH_HEAD")\r
- else:\r
- raise\r
-\r
- print("Switched to branch \"%s\"" % branch_name)\r
-\r
-\r
-class PatchSetGitCherrypickFailed(CommandFailed):\r
- "There was a problem applying changeset contents to the current branch."\r
- EXIT_CODE = 69\r
-\r
-\r
-def cherrypick_review(option=None):\r
- cmd = ["git", "cherry-pick"]\r
- if option:\r
- cmd.append(option)\r
- cmd.append("FETCH_HEAD")\r
- print(run_command_exc(PatchSetGitCherrypickFailed, *cmd))\r
-\r
-\r
-class CheckoutBackExistingBranchFailed(CommandFailed):\r
- "Cannot switch back to existing branch"\r
- EXIT_CODE = 67\r
-\r
-\r
-class DeleteBranchFailed(CommandFailed):\r
- "Failed to delete branch"\r
- EXIT_CODE = 68\r
-\r
-\r
-class InvalidPatchsetsToCompare(GitReviewException):\r
- def __init__(self, patchsetA, patchsetB):\r
- Exception.__init__(\r
- self,\r
- "Invalid patchsets for comparison specified (old=%s,new=%s)" % (\r
- patchsetA,\r
- patchsetB))\r
- EXIT_CODE = 39\r
-\r
-\r
-def compare_review(review_spec, branch, remote, rebase=False):\r
- new_ps = None # none means latest\r
-\r
- if '-' in review_spec:\r
- review_spec, new_ps = review_spec.split('-')\r
- review, old_ps = parse_review_number(review_spec)\r
-\r
- if old_ps is None or old_ps == new_ps:\r
- raise InvalidPatchsetsToCompare(old_ps, new_ps)\r
-\r
- old_review = build_review_number(review, old_ps)\r
- new_review = build_review_number(review, new_ps)\r
-\r
- old_branch = fetch_review(old_review, branch, remote)\r
- checkout_review(old_branch)\r
-\r
- if rebase:\r
- print('Rebasing %s' % old_branch)\r
- rebase = rebase_changes(branch, remote, False)\r
- if not rebase:\r
- print('Skipping rebase because of conflicts')\r
- run_command_exc(CommandFailed, 'git', 'rebase', '--abort')\r
-\r
- new_branch = fetch_review(new_review, branch, remote)\r
- checkout_review(new_branch)\r
-\r
- if rebase:\r
- print('Rebasing also %s' % new_branch)\r
- if not rebase_changes(branch, remote, False):\r
- print("Rebasing of the new branch failed, "\r
- "diff can be messed up (use -R to not rebase at all)!")\r
- run_command_exc(CommandFailed, 'git', 'rebase', '--abort')\r
-\r
- subprocess.check_call(['git', 'difftool', old_branch])\r
-\r
-\r
-def finish_branch(target_branch):\r
- local_branch = get_branch_name(target_branch)\r
- if VERBOSE:\r
- print("Switching back to '%s' and deleting '%s'" % (target_branch,\r
- local_branch))\r
- run_command_exc(CheckoutBackExistingBranchFailed,\r
- "git", "checkout", target_branch)\r
- print("Switched to branch '%s'" % target_branch)\r
-\r
- run_command_exc(DeleteBranchFailed,\r
- "git", "branch", "-D", local_branch)\r
- print("Deleted branch '%s'" % local_branch)\r
-\r
-\r
-def convert_bool(one_or_zero):\r
- "Return a bool on a one or zero string."\r
- return one_or_zero in ["1", "true", "True"]\r
-\r
-\r
-def print_exit_message(status, needs_update):\r
-\r
- if needs_update:\r
- print("""\r
-***********************************************************\r
-A new version of git-review is available on PyPI. Please\r
-update your copy with:\r
-\r
- pip install -U git-review\r
-\r
-to ensure proper behavior with gerrit. Thanks!\r
-***********************************************************\r
-""")\r
- sys.exit(status)\r
-\r
-\r
-def main():\r
- global _no_color_support\r
- usage = "git review [OPTIONS] ... [BRANCH]"\r
-\r
- import argparse\r
-\r
- class DownloadFlag(argparse.Action):\r
- """Additional option parsing: store value in 'dest', but\r
- at the same time set one of the flag options to True\r
- """\r
- def __call__(self, parser, namespace, values, option_string=None):\r
- setattr(namespace, self.dest, values)\r
- setattr(namespace, self.const, True)\r
-\r
- parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT)\r
-\r
- parser.add_argument("-t", "--topic", dest="topic",\r
- help="Topic to submit branch to")\r
- parser.add_argument("-D", "--draft", dest="draft", action="store_true",\r
- help="Submit review as a draft")\r
- parser.add_argument("-c", "--compatible", dest="compatible",\r
- action="store_true",\r
- help="Push change to refs/for/* for compatibility "\r
- "with Gerrit versions < 2.3. Ignored if "\r
- "-D/--draft is used.")\r
- parser.add_argument("-n", "--dry-run", dest="dry", action="store_true",\r
- help="Don't actually submit the branch for review")\r
- parser.add_argument("-i", "--new-changeid", dest="regenerate",\r
- action="store_true",\r
- help="Regenerate Change-id before submitting")\r
- parser.add_argument("-r", "--remote", dest="remote",\r
- help="git remote to use for gerrit")\r
- parser.add_argument("-R", "--no-rebase", dest="rebase",\r
- action="store_false",\r
- help="Don't rebase changes before submitting.")\r
- parser.add_argument("-F", "--force-rebase", dest="force_rebase",\r
- action="store_true",\r
- help="Force rebase even when not needed.")\r
- parser.add_argument("-B", "--no-color", dest="no_color_support",\r
- action="store_true",\r
- help="No color support.")\r
- \r
-\r
- fetch = parser.add_mutually_exclusive_group()\r
- fetch.set_defaults(download=False, compare=False, cherrypickcommit=False,\r
- cherrypickindicate=False, cherrypickonly=False)\r
- fetch.add_argument("-d", "--download", dest="changeidentifier",\r
- action=DownloadFlag, metavar="CHANGE",\r
- const="download",\r
- help="Download the contents of an existing gerrit "\r
- "review into a branch")\r
- fetch.add_argument("-x", "--cherrypick", dest="changeidentifier",\r
- action=DownloadFlag, metavar="CHANGE",\r
- const="cherrypickcommit",\r
- help="Apply the contents of an existing gerrit "\r
- "review onto the current branch and commit "\r
- "(cherry pick; not recommended in most "\r
- "situations)")\r
- fetch.add_argument("-X", "--cherrypickindicate", dest="changeidentifier",\r
- action=DownloadFlag, metavar="CHANGE",\r
- const="cherrypickindicate",\r
- help="Apply the contents of an existing gerrit "\r
- "review onto the current branch and commit, "\r
- "indicating its origin")\r
- fetch.add_argument("-N", "--cherrypickonly", dest="changeidentifier",\r
- action=DownloadFlag, metavar="CHANGE",\r
- const="cherrypickonly",\r
- help="Apply the contents of an existing gerrit "\r
- "review to the working directory and prepare "\r
- "for commit")\r
- fetch.add_argument("-m", "--compare", dest="changeidentifier",\r
- action=DownloadFlag, metavar="CHANGE,PS[-NEW_PS]",\r
- const="compare",\r
- help="Download specified and latest (or NEW_PS) "\r
- "patchsets of an existing gerrit review into "\r
- "a branches, rebase on master "\r
- "(skipped on conflicts or when -R is specified) "\r
- "and show their differences")\r
-\r
- parser.add_argument("-u", "--update", dest="update", action="store_true",\r
- help="Force updates from remote locations")\r
- parser.add_argument("-s", "--setup", dest="setup", action="store_true",\r
- help="Just run the repo setup commands but don't "\r
- "submit anything")\r
- parser.add_argument("-f", "--finish", dest="finish", action="store_true",\r
- help="Close down this branch and switch back to "\r
- "master on successful submission")\r
- parser.add_argument("-l", "--list", dest="list", action="store_true",\r
- help="List available reviews for the current project")\r
- parser.add_argument("-y", "--yes", dest="yes", action="store_true",\r
- help="Indicate that you do, in fact, understand if "\r
- "you are submitting more than one patch")\r
- parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",\r
- help="Output more information about what's going on")\r
- parser.add_argument("--no-custom-script", dest="custom_script",\r
- action="store_false", default=True,\r
- help="Do not run custom scripts.")\r
- parser.add_argument("--license", dest="license", action="store_true",\r
- help="Print the license and exit")\r
- parser.add_argument("--version", action="version",\r
- version='%s version %s' %\r
- (os.path.split(sys.argv[0])[-1], version))\r
- parser.add_argument("branch", nargs="?")\r
-\r
- try:\r
- (top_dir, git_dir) = git_directories()\r
- except GitDirectoriesException:\r
- if sys.argv[1:] in ([], ['-h'], ['--help']):\r
- parser.print_help()\r
- sys.exit(1)\r
- raise\r
-\r
-# config = get_config(os.path.join(top_dir, ".gitreview"))\r
- config = DEFAULTS.copy() \r
- cur_proj = run_command("git remote -v")\r
- import re\r
- m = re.search('(?<=29418\/)(\S+)', cur_proj)\r
- config['project'] = m.group(0) \r
-\r
- \r
- defaultrebase = convert_bool(\r
- git_config_get_value("gitreview", "rebase",\r
- default=str(config['defaultrebase'])))\r
- parser.set_defaults(dry=False,\r
- draft=False,\r
- rebase=defaultrebase,\r
- verbose=False,\r
- update=False,\r
- setup=False,\r
- list=False,\r
- yes=False,\r
- branch=config['defaultbranch'],\r
- remote=config['defaultremote'])\r
- options = parser.parse_args()\r
-\r
- if options.license:\r
- print(COPYRIGHT)\r
- sys.exit(0)\r
-\r
- branch = options.branch\r
- global VERBOSE\r
- global UPDATE\r
- VERBOSE = options.verbose\r
- UPDATE = options.update\r
- remote = options.remote\r
- yes = options.yes\r
- status = 0\r
-\r
- needs_update = latest_is_newer()\r
- check_remote(branch, remote,\r
- config['hostname'], config['port'], config['project'])\r
-\r
- if options.no_color_support:\r
- _no_color_support = True\r
-\r
- if options.changeidentifier:\r
- if options.compare:\r
- compare_review(options.changeidentifier,\r
- branch, remote, options.rebase)\r
- return\r
- local_branch = fetch_review(options.changeidentifier, branch, remote)\r
- if options.download:\r
- checkout_review(local_branch)\r
- else:\r
- if options.cherrypickcommit:\r
- cherrypick_review()\r
- elif options.cherrypickonly:\r
- cherrypick_review("-n")\r
- if options.cherrypickindicate:\r
- cherrypick_review("-x")\r
- return\r
- elif options.list:\r
- list_reviews(remote)\r
- return\r
-\r
- if options.custom_script:\r
- run_custom_script("pre")\r
-\r
- hook_file = os.path.join(git_dir, "hooks", "commit-msg")\r
- have_hook = os.path.exists(hook_file) and os.access(hook_file, os.X_OK)\r
-\r
- if not have_hook:\r
- set_hooks_commit_msg(remote, hook_file)\r
-\r
- if options.setup:\r
- if options.finish and not options.dry:\r
- finish_branch(branch)\r
- return\r
-\r
- if options.rebase:\r
- if not rebase_changes(branch, remote):\r
- print_exit_message(1, needs_update)\r
- if not options.force_rebase and not undo_rebase():\r
- print_exit_message(1, needs_update)\r
- assert_one_change(remote, branch, yes, have_hook)\r
-\r
- ref = "publish"\r
-\r
- if options.draft:\r
- ref = "drafts"\r
- if options.custom_script:\r
- run_custom_script("draft")\r
- elif options.compatible:\r
- ref = "for"\r
-\r
- cmd = "git push %s HEAD:refs/%s/%s" % (remote, ref, branch)\r
- topic = options.topic or get_topic(branch)\r
- if topic != branch:\r
- cmd += "/%s" % topic\r
- if options.regenerate:\r
- print("Amending the commit to regenerate the change id\n")\r
- regenerate_cmd = "git commit --amend"\r
- if options.dry:\r
- print("\tGIT_EDITOR=\"sed -i -e '/^Change-Id:/d'\" %s\n" %\r
- regenerate_cmd)\r
- else:\r
- run_command(regenerate_cmd,\r
- GIT_EDITOR="sed -i -e "\r
- "'/^Change-Id:/d'")\r
-\r
- if options.dry:\r
- print("Please use the following command "\r
- "to send your commits to review:\n")\r
- print("\t%s\n" % cmd)\r
- else:\r
- (status, output) = run_command_status(cmd)\r
- print(output)\r
-\r
- if options.finish and not options.dry and status == 0:\r
- finish_branch(branch)\r
- return\r
-\r
- if options.custom_script:\r
- run_custom_script("post")\r
- print_exit_message(status, needs_update)\r
-\r
-\r
-if __name__ == "__main__":\r
- try:\r
- main()\r
- except GitReviewException as e:\r
- print(e)\r
- sys.exit(e.EXIT_CODE)\r
+#!/usr/bin/env python
+from __future__ import print_function
+
+COPYRIGHT = """\
+Copyright (C) 2011-2012 OpenStack LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied.
+
+See the License for the specific language governing permissions and
+limitations under the License."""
+
+import datetime
+import json
+import os
+import re
+import shlex
+import subprocess
+import sys
+import time
+
+if sys.version < '3':
+ import ConfigParser
+ import urllib
+ import urlparse
+ urlopen = urllib.urlopen
+ urlparse = urlparse.urlparse
+ do_input = raw_input
+else:
+ import configparser as ConfigParser
+ import urllib.parse
+ import urllib.request
+ urlopen = urllib.request.urlopen
+ urlparse = urllib.parse.urlparse
+ do_input = input
+
+from distutils import version as du_version
+
+version = "1.22"
+
+VERBOSE = False
+UPDATE = False
+CONFIGDIR = os.path.expanduser("~/.config/git-review")
+GLOBAL_CONFIG = "/etc/git-review/git-review.conf"
+USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf")
+PYPI_URL = "http://pypi.python.org/pypi/git-review/json"
+PYPI_CACHE_TIME = 60 * 60 * 24 # 24 hours
+DEFAULTS = dict(hostname='gerrit.lud.stericsson.com', port='29418', project=False,
+ defaultbranch='master', defaultremote="gerrit",
+ defaultrebase="0")
+
+_branch_name = None
+_has_color = None
+_no_color_support = False
+
+
+class colors:
+ yellow = '\033[33m'
+ green = '\033[92m'
+ reset = '\033[0m'
+
+
+class GitReviewException(Exception):
+ pass
+
+
+class CommandFailed(GitReviewException):
+
+ def __init__(self, *args):
+ Exception.__init__(self, *args)
+ (self.rc, self.output, self.argv, self.envp) = args
+ self.quickmsg = dict([
+ ("argv", " ".join(self.argv)),
+ ("rc", self.rc),
+ ("output", self.output)])
+
+ def __str__(self):
+ return self.__doc__ + """
+The following command failed with exit code %(rc)d
+ "%(argv)s"
+-----------------------
+%(output)s
+-----------------------""" % self.quickmsg
+
+
+class ChangeSetException(GitReviewException):
+
+ def __init__(self, e):
+ GitReviewException.__init__(self)
+ self.e = str(e)
+
+ def __str__(self):
+ return self.__doc__ % self.e
+
+
+def parse_review_number(review):
+ parts = review.split(',')
+ if len(parts) < 2:
+ parts.append(None)
+ return parts
+
+
+def build_review_number(review, patchset):
+ if patchset is not None:
+ return '%s,%s' % (review, patchset)
+ return review
+
+
+def run_command_status(*argv, **env):
+ if VERBOSE:
+ print(datetime.datetime.now(), "Running:", " ".join(argv))
+ if len(argv) == 1:
+ argv = shlex.split(str(argv[0]))
+ newenv = os.environ
+ newenv.update(env)
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, env=newenv)
+ (out, nothing) = p.communicate()
+ out = out.decode('utf-8')
+ return (p.returncode, out.strip())
+
+
+def run_command(*argv, **env):
+ (rc, output) = run_command_status(*argv, **env)
+ return output
+
+
+def run_command_exc(klazz, *argv, **env):
+ """Run command *argv, on failure raise 'klazz
+
+ klass should be derived from CommandFailed
+ """
+ (rc, output) = run_command_status(*argv, **env)
+ if rc != 0:
+ raise klazz(rc, output, argv, env)
+ return output
+
+
+def update_latest_version(version_file_path):
+ """Cache the latest version of git-review for the upgrade check."""
+
+ if not os.path.exists(CONFIGDIR):
+ os.makedirs(CONFIGDIR)
+
+ if os.path.exists(version_file_path) and not UPDATE:
+ if (time.time() - os.path.getmtime(version_file_path)) < 28800:
+ return
+
+ latest_version = version
+ try:
+ latest_version = json.load(urlopen(PYPI_URL))['info']['version']
+ except Exception:
+ pass
+
+ with open(version_file_path, "w") as version_file:
+ version_file.write(latest_version)
+
+
+def latest_is_newer():
+ """Check if there is a new version of git-review."""
+
+ # Skip version check if distro package turns it off
+ if os.path.exists(GLOBAL_CONFIG):
+ config = dict(check=False)
+ configParser = ConfigParser.ConfigParser(config)
+ configParser.read(GLOBAL_CONFIG)
+ if not configParser.getboolean("updates", "check"):
+ return False
+
+ version_file_path = os.path.join(CONFIGDIR, "latest-version")
+ update_latest_version(version_file_path)
+
+ latest_version = None
+ with open(version_file_path, "r") as version_file:
+ latest_version = du_version.StrictVersion(version_file.read())
+ if latest_version > du_version.StrictVersion(version):
+ return True
+ return False
+
+
+def git_directories():
+ """Determine (absolute git work directory path, .git subdirectory path)."""
+ cmd = ("git", "rev-parse", "--show-toplevel", "--git-dir")
+ out = run_command_exc(GitDirectoriesException, *cmd)
+ try:
+ return out.split()
+ except ValueError:
+ raise GitDirectoriesException(0, out, cmd, {})
+
+
+class GitDirectoriesException(CommandFailed):
+ "Cannot determine where .git directory is."
+ EXIT_CODE = 70
+
+
+class CustomScriptException(CommandFailed):
+ """Custom script execution failed."""
+ EXIT_CODE = 71
+
+
+def run_custom_script(action):
+ """Get status and output of .git/hooks/$action-review or/and
+ ~/.config/hooks/$action-review if existing.
+ """
+ returns = []
+ script_file = "%s-review" % (action)
+ (top_dir, git_dir) = git_directories()
+ paths = [os.path.join(CONFIGDIR, "hooks", script_file),
+ os.path.join(git_dir, "hooks", script_file)]
+ for fpath in paths:
+ if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
+ status, output = run_command_status(fpath)
+ returns.append((status, output, fpath))
+
+ for (status, output, path) in returns:
+ if status is not None and status != 0:
+ raise CustomScriptException(status, output, [path], {})
+ elif output and VERBOSE:
+ print("script %s output is:" % (path))
+ print(output)
+
+
+def git_config_get_value(section, option, default=None):
+ try:
+ return run_command_exc(GitConfigException,
+ "git", "config",
+ "--get",
+ "%s.%s" % (section, option)).strip()
+ except GitConfigException as exc:
+ if exc.rc == 1:
+ return default
+ raise
+
+
+class GitConfigException(CommandFailed):
+ """Git config value retrieval failed."""
+ EXIT_CODE = 128
+
+
+class CannotInstallHook(CommandFailed):
+ "Problems encountered installing commit-msg hook"
+ EXIT_CODE = 2
+
+
+def set_hooks_commit_msg(remote, target_file):
+ """Install the commit message hook if needed."""
+
+ # Create the hooks directory if it's not there already
+ hooks_dir = os.path.dirname(target_file)
+ if not os.path.isdir(hooks_dir):
+ os.mkdir(hooks_dir)
+
+ (hostname, username, port, project_name) = \
+ parse_git_show(remote, "Push")
+
+ if not os.path.exists(target_file) or UPDATE:
+ if VERBOSE:
+ print("Fetching commit hook from: scp://%s:%s" % (hostname, port))
+ if port is not None:
+ port = "-P %s" % port
+ else:
+ port = ""
+ if username is None:
+ userhost = hostname
+ else:
+ userhost = "%s@%s" % (username, hostname)
+ run_command_exc(
+ CannotInstallHook,
+ "scp", port,
+ userhost + ":hooks/commit-msg",
+ target_file)
+
+ if not os.access(target_file, os.X_OK):
+ os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC)
+
+
+def test_remote(username, hostname, port, project):
+ """Tests that a possible gerrit remote works."""
+
+ if port is not None:
+ port = "-p %s" % port
+ else:
+ port = ""
+ if username is None:
+ userhost = hostname
+ else:
+ userhost = "%s@%s" % (username, hostname)
+
+ (status, ssh_output) = run_command_status(
+ "ssh", "-x", port, userhost,
+ "gerrit", "ls-projects")
+
+ if status == 0:
+ if VERBOSE:
+ print("%s@%s:%s worked." % (username, hostname, port))
+ return True
+ else:
+ if VERBOSE:
+ print("%s@%s:%s did not work." % (username, hostname, port))
+ return False
+
+
+def make_remote_url(username, hostname, port, project):
+ """Builds a gerrit remote URL."""
+ if username is None:
+ return "ssh://%s:%s/%s" % (hostname, port, project)
+ else:
+ return "ssh://%s@%s:%s/%s" % (username, hostname, port, project)
+
+
+def add_remote(hostname, port, project, remote):
+ """Adds a gerrit remote."""
+ asked_for_username = False
+
+ username = os.getenv("USERNAME")
+ if not username:
+ username = git_config_get_value("gitreview", "username")
+ if not username:
+ username = os.getenv("USER")
+ if port is None:
+ port = 29418
+
+ remote_url = make_remote_url(username, hostname, port, project)
+ if VERBOSE:
+ print("No remote set, testing %s" % remote_url)
+ if not test_remote(username, hostname, port, project):
+ print("Could not connect to gerrit.")
+ username = do_input("Enter your gerrit username: ")
+ remote_url = make_remote_url(username, hostname, port, project)
+ print("Trying again with %s" % remote_url)
+ if not test_remote(username, hostname, port, project):
+ raise Exception("Could not connect to gerrit at %s" % remote_url)
+ asked_for_username = True
+
+ print("Creating a git remote called \"%s\" that maps to:" % remote)
+ print("\t%s" % remote_url)
+ cmd = "git remote add -f %s %s" % (remote, remote_url)
+ (status, remote_output) = run_command_status(cmd)
+
+ if status != 0:
+ raise Exception("Error running %s" % cmd)
+
+ if asked_for_username:
+ print()
+ print("This repository is now set up for use with git-review.")
+ print("You can set the default username for future repositories with:")
+ print(' git config --global --add gitreview.username "%s"' % username)
+ print()
+
+
+def parse_git_show(remote, verb):
+ fetch_url = ""
+ for line in run_command("git remote show -n %s" % remote).split("\n"):
+ if line.strip().startswith("%s" % verb):
+ fetch_url = line.split()[2]
+
+ parsed_url = urlparse(fetch_url)
+ project_name = parsed_url.path.lstrip("/")
+ if project_name.endswith(".git"):
+ project_name = project_name[:-4]
+
+ hostname = parsed_url.netloc
+ username = None
+ port = parsed_url.port
+
+ if VERBOSE:
+ print("Found origin %s URL:" % verb, fetch_url)
+
+ # Workaround bug in urlparse on OSX
+ if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
+ hostname = parsed_url.path[2:].split("/")[0]
+
+ if "@" in hostname:
+ (username, hostname) = hostname.split("@")
+ if ":" in hostname:
+ (hostname, port) = hostname.split(":")
+
+ # Is origin an ssh location? Let's pull more info
+ if parsed_url.scheme == "ssh" and port is None:
+ port = 22
+
+ return (hostname, username, str(port), project_name)
+
+
+def check_color_support():
+ global _has_color
+ global _no_color_support
+ if _has_color is None:
+ test_command = "git log --color=never --oneline HEAD^1..HEAD"
+ (status, output) = run_command_status(test_command)
+ if status == 0:
+ _has_color = True
+ else:
+ _has_color = False
+
+ if _no_color_support:
+ _has_color = False
+
+ return _has_color
+
+
+def get_config(config_file=None):
+ """Generate the configuration map by starting with some built-in defaults
+ and then loading GLOBAL_CONFIG, USER_CONFIG, and a repository-specific
+ .gitreview file, if they exist. In case of conflict, the configuration file
+ with the narrowest scope wins.
+ """
+ config = DEFAULTS.copy()
+ for filename in (GLOBAL_CONFIG, USER_CONFIG, config_file):
+ if filename is not None and os.path.exists(filename):
+ config.update(load_config_file(filename))
+ return config
+
+
+def load_config_file(config_file):
+ """Load configuration options from a file."""
+ configParser = ConfigParser.ConfigParser()
+ configParser.read(config_file)
+ options = {
+ 'hostname': 'host',
+ 'port': 'port',
+ 'project': 'project',
+ 'defaultbranch': 'defaultbranch',
+ 'defaultremote': 'defaultremote',
+ 'defaultrebase': 'defaultrebase',
+ }
+ config = {}
+ for config_key, option_name in options.items():
+ if configParser.has_option('gerrit', option_name):
+ config[config_key] = configParser.get('gerrit', option_name)
+ return config
+
+
+def update_remote(remote):
+ cmd = "git remote update %s" % remote
+ (status, output) = run_command_status(cmd)
+ if VERBOSE:
+ print(output)
+ if status != 0:
+ print("Problem running '%s'" % cmd)
+ if not VERBOSE:
+ print(output)
+ return False
+ return True
+
+
+def check_remote(branch, remote, hostname, port, project):
+ """Check that a Gerrit Git remote repo exists, if not, set one."""
+
+ has_color = check_color_support()
+ if has_color:
+ color_never = "--color=never"
+ else:
+ color_never = ""
+
+ if remote in run_command("git remote").split("\n"):
+
+ remotes = run_command("git branch -a %s" % color_never).split("\n")
+ for current_remote in remotes:
+ if (current_remote.strip() == "remotes/%s/%s" % (remote, branch)
+ and not UPDATE):
+ return
+ # We have the remote, but aren't set up to fetch. Fix it
+ if VERBOSE:
+ print("Setting up gerrit branch tracking for better rebasing")
+ update_remote(remote)
+ return
+
+ if hostname is False or port is False or project is False:
+ # This means there was no .gitreview file
+ print("No '.gitreview' file found in this repository.")
+ print("We don't know where your gerrit is. Please manually create ")
+ print("a remote named \"%s\" and try again." % remote)
+ sys.exit(1)
+
+ # Gerrit remote not present, try to add it
+ try:
+ add_remote(hostname, port, project, remote)
+ except Exception:
+ print(sys.exc_info()[2])
+ print("We don't know where your gerrit is. Please manually create ")
+ print("a remote named \"%s\" and try again." % remote)
+ raise
+
+
+def rebase_changes(branch, remote, interactive=True):
+
+ remote_branch = "remotes/%s/%s" % (remote, branch)
+
+ if not update_remote(remote):
+ return False
+
+ if interactive:
+ cmd = "git rebase -i %s" % remote_branch
+ else:
+ cmd = "git rebase %s" % remote_branch
+
+ (status, output) = run_command_status(cmd, GIT_EDITOR='true')
+ if status != 0:
+ print("Errors running %s" % cmd)
+ if interactive:
+ print(output)
+ return False
+ return True
+
+
+def undo_rebase():
+ cmd = "git reset --hard ORIG_HEAD"
+ (status, output) = run_command_status(cmd)
+ if status != 0:
+ print("Errors running %s" % cmd)
+ print(output)
+ return False
+ return True
+
+
+def get_branch_name(target_branch):
+ global _branch_name
+ if _branch_name is not None:
+ return _branch_name
+ _branch_name = None
+ has_color = check_color_support()
+ if has_color:
+ color_never = "--color=never"
+ else:
+ color_never = ""
+ for branch in run_command("git branch %s" % color_never).split("\n"):
+ if branch.startswith('*'):
+ _branch_name = branch.split()[1].strip()
+ if _branch_name == "(no":
+ _branch_name = target_branch
+ return _branch_name
+
+
+def assert_one_change(remote, branch, yes, have_hook):
+ has_color = check_color_support()
+ if has_color:
+ color = git_config_get_value("color", "ui")
+ if color is None:
+ color = "auto"
+ else:
+ color = color.lower()
+ if (color == "" or color == "true"):
+ color = "auto"
+ elif color == "false":
+ color = "never"
+ elif color == "auto":
+ # Python is not a tty, we have to force colors
+ color = "always"
+ use_color = "--color=%s" % color
+ else:
+ use_color = ""
+ cmd = "git log %s --decorate --oneline HEAD --not remotes/%s/%s --" % (
+ use_color, remote, branch)
+ (status, output) = run_command_status(cmd)
+ if status != 0:
+ print("Had trouble running %s" % cmd)
+ print(output)
+ sys.exit(1)
+ output_lines = len(output.split("\n"))
+ if output_lines == 1 and not have_hook:
+ print("Your change was committed before the commit hook was installed")
+ print("Amending the commit to add a gerrit change id")
+ run_command("git commit --amend", GIT_EDITOR='true')
+ elif output_lines == 0:
+ print("No changes between HEAD and %s/%s." % (remote, branch))
+ print("Submitting for review would be pointless.")
+ sys.exit(1)
+ elif output_lines > 1:
+ if not yes:
+ print("You have more than one commit"
+ " that you are about to submit.")
+ print("The outstanding commits are:\n\n%s\n" % output)
+ print("Is this really what you meant to do?")
+ yes_no = do_input("Type 'yes' to confirm: ")
+ if yes_no.lower().strip() != "yes":
+ print("Aborting.")
+ print("Please rebase/squash your changes and try again")
+ sys.exit(1)
+
+
+def use_topic(why, topic):
+ """Inform the user about why a particular topic has been selected."""
+ if VERBOSE:
+ print(why % ('"%s"' % topic,))
+ return topic
+
+
+def get_topic(target_branch):
+
+ branch_name = get_branch_name(target_branch)
+
+ branch_parts = branch_name.split("/")
+ if len(branch_parts) >= 3 and branch_parts[0] == "review":
+ return use_topic("Using change number %s "
+ "for the topic of the change submitted",
+ "/".join(branch_parts[2:]))
+
+ log_output = run_command("git log HEAD^1..HEAD")
+ bug_re = r'\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)'
+
+ match = re.search(bug_re, log_output)
+ if match is not None:
+ return use_topic("Using bug number %s "
+ "for the topic of the change submitted",
+ "bug/%s" % match.group(2))
+
+ bp_re = r'\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)'
+ match = re.search(bp_re, log_output)
+ if match is not None:
+ return use_topic("Using blueprint number %s "
+ "for the topic of the change submitted",
+ "bp/%s" % match.group(2))
+
+ return use_topic("Using local branch name %s "
+ "for the topic of the change submitted",
+ branch_name)
+
+
+class CannotQueryOpenChangesets(CommandFailed):
+ "Cannot fetch review information from gerrit"
+ EXIT_CODE = 32
+
+
+class CannotParseOpenChangesets(ChangeSetException):
+ "Cannot parse JSON review information from gerrit"
+ EXIT_CODE = 33
+
+
+def list_reviews(remote):
+
+ (hostname, username, port, project_name) = \
+ parse_git_show(remote, "Push")
+
+ if port is not None:
+ port = "-p %s" % port
+ else:
+ port = ""
+ if username is None:
+ userhost = hostname
+ else:
+ userhost = "%s@%s" % (username, hostname)
+
+ review_info = None
+ output = run_command_exc(
+ CannotQueryOpenChangesets,
+ "ssh", "-x", port, userhost,
+ "gerrit", "query",
+ "--current-patch-set --format=JSON project:%s status:open reviewer:self" % project_name)
+
+ review_list = []
+ review_field_width = {}
+ REVIEW_FIELDS = ('number', 'currentPatchSet', 'branch', 'subject')
+ FIELDS = range(0, len(REVIEW_FIELDS))
+ if check_color_support():
+ review_field_color = (colors.yellow, colors.yellow, colors.green, "")
+ color_reset = colors.reset
+ else:
+ review_field_color = ("", "", "", "")
+ color_reset = ""
+ review_field_width = [0, 0, 0, 0]
+ review_field_format = ["%*s", "%*s", "%*s", "%*s"]
+ review_field_justify = [+1, +1, +1, -1] # -1 is justify to right
+
+ for line in output.split("\n"):
+ # Warnings from ssh wind up in this output
+ if line[0] != "{":
+ print(line)
+ continue
+ try:
+ review_info = json.loads(line)
+ except Exception:
+ if VERBOSE:
+ print(output)
+ raise(CannotParseOpenChangesets, sys.exc_info()[1])
+
+ if 'type' in review_info:
+ break
+
+ tempPS = review_info['currentPatchSet']
+ appPS = tempPS['approvals']
+ appValue = '-';
+ for appLine in appPS:
+ appBy = appLine['by']
+ appUser = appBy['username']
+ if appUser == username:
+ appValue = appLine['value']
+ review_info['currentPatchSet'] = tempPS['number'] + ' ' + appValue
+
+ review_list.append([review_info[f] for f in REVIEW_FIELDS])
+ for i in FIELDS:
+ review_field_width[i] = max(
+ review_field_width[i],
+ len(review_info[REVIEW_FIELDS[i]])
+ )
+
+ review_field_format = " ".join([
+ review_field_color[i] +
+ review_field_format[i] +
+ color_reset
+ for i in FIELDS])
+
+ review_field_width = [
+ review_field_width[i] * review_field_justify[i]
+ for i in FIELDS]
+ for review_value in review_list:
+ # At this point we have review_field_format
+ # like "%*s %*s %*s" and we need to supply
+ # (width1, value1, width2, value2, ...) tuple to print
+ # It's easy to zip() widths with actual values,
+ # but we need to flatten the resulting
+ # ((width1, value1), (width2, value2), ...) map.
+ formatted_fields = []
+ for (width, value) in zip(review_field_width, review_value):
+ formatted_fields.extend([width, value])
+ print(review_field_format % tuple(formatted_fields))
+ print("Found %d items for review" % review_info['rowCount'])
+
+ return 0
+
+
+class CannotQueryPatchSet(CommandFailed):
+ "Cannot query patchset information"
+ EXIT_CODE = 34
+
+
+class ReviewInformationNotFound(ChangeSetException):
+ "Could not fetch review information for change %s"
+ EXIT_CODE = 35
+
+
+class ReviewNotFound(ChangeSetException):
+ "Gerrit review %s not found"
+ EXIT_CODE = 36
+
+
+class PatchSetGitFetchFailed(CommandFailed):
+ """Cannot fetch patchset contents
+
+Does specified change number belong to this project?
+"""
+ EXIT_CODE = 37
+
+
+class PatchSetNotFound(ChangeSetException):
+ "Review patchset %s not found"
+ EXIT_CODE = 38
+
+
+class CheckoutNewBranchFailed(CommandFailed):
+ "Cannot checkout to new branch"
+ EXIT_CODE = 64
+
+
+class CheckoutExistingBranchFailed(CommandFailed):
+ "Cannot checkout existing branch"
+ EXIT_CODE = 65
+
+
+class ResetHardFailed(CommandFailed):
+ "Failed to hard reset downloaded branch"
+ EXIT_CODE = 66
+
+
+def fetch_review(review, masterbranch, remote):
+
+ (hostname, username, port, project_name) = \
+ parse_git_show(remote, "Push")
+
+ if port is not None:
+ port = "-p %s" % port
+ else:
+ port = ""
+ if username is None:
+ userhost = hostname
+ else:
+ userhost = "%s@%s" % (username, hostname)
+
+ review_arg = review
+ patchset_opt = '--current-patch-set'
+
+ review, patchset_number = parse_review_number(review)
+ if patchset_number is not None:
+ patchset_opt = '--patch-sets'
+
+ review_info = None
+ output = run_command_exc(
+ CannotQueryPatchSet,
+ "ssh", "-x", port, userhost,
+ "gerrit", "query",
+ "--format=JSON %s change:%s" % (patchset_opt, review))
+
+ review_jsons = output.split("\n")
+ found_review = False
+ for review_json in review_jsons:
+ try:
+ review_info = json.loads(review_json)
+ found_review = True
+ except Exception:
+ pass
+ if found_review:
+ break
+ if not found_review:
+ if VERBOSE:
+ print(output)
+ raise ReviewInformationNotFound(review)
+
+ try:
+ if patchset_number is None:
+ refspec = review_info['currentPatchSet']['ref']
+ else:
+ refspec = [ps for ps
+ in review_info['patchSets']
+ if ps['number'] == patchset_number][0]['ref']
+ except IndexError:
+ raise PatchSetNotFound(review_arg)
+ except KeyError:
+ raise ReviewNotFound(review)
+
+ try:
+ topic = review_info['topic']
+ if topic == masterbranch:
+ topic = review
+ except KeyError:
+ topic = review
+ try:
+ author = re.sub('\W+', '_', review_info['owner']['name']).lower()
+ except KeyError:
+ author = 'unknown'
+
+ if patchset_number is None:
+ branch_name = "review/%s/%s" % (author, topic)
+ else:
+ branch_name = "review/%s/%s-patch%s" % (author, topic, patchset_number)
+
+ print("Downloading %s from gerrit" % refspec)
+ run_command_exc(PatchSetGitFetchFailed,
+ "git", "fetch", remote, refspec)
+ return branch_name
+
+
+def checkout_review(branch_name):
+ """Checkout a newly fetched (FETCH_HEAD) change
+ into a branch
+ """
+
+ try:
+ run_command_exc(CheckoutNewBranchFailed,
+ "git", "checkout", "-b",
+ branch_name, "FETCH_HEAD")
+
+ except CheckoutNewBranchFailed as e:
+ if re.search("already exists\.?", e.output):
+ print("Branch already exists - reusing")
+ run_command_exc(CheckoutExistingBranchFailed,
+ "git", "checkout", branch_name)
+ run_command_exc(ResetHardFailed,
+ "git", "reset", "--hard", "FETCH_HEAD")
+ else:
+ raise
+
+ print("Switched to branch \"%s\"" % branch_name)
+
+
+class PatchSetGitCherrypickFailed(CommandFailed):
+ "There was a problem applying changeset contents to the current branch."
+ EXIT_CODE = 69
+
+
+def cherrypick_review(option=None):
+ cmd = ["git", "cherry-pick"]
+ if option:
+ cmd.append(option)
+ cmd.append("FETCH_HEAD")
+ print(run_command_exc(PatchSetGitCherrypickFailed, *cmd))
+
+
+class CheckoutBackExistingBranchFailed(CommandFailed):
+ "Cannot switch back to existing branch"
+ EXIT_CODE = 67
+
+
+class DeleteBranchFailed(CommandFailed):
+ "Failed to delete branch"
+ EXIT_CODE = 68
+
+
+class InvalidPatchsetsToCompare(GitReviewException):
+ def __init__(self, patchsetA, patchsetB):
+ Exception.__init__(
+ self,
+ "Invalid patchsets for comparison specified (old=%s,new=%s)" % (
+ patchsetA,
+ patchsetB))
+ EXIT_CODE = 39
+
+
+def compare_review(review_spec, branch, remote, rebase=False):
+ new_ps = None # none means latest
+
+ if '-' in review_spec:
+ review_spec, new_ps = review_spec.split('-')
+ review, old_ps = parse_review_number(review_spec)
+
+ if old_ps is None or old_ps == new_ps:
+ raise InvalidPatchsetsToCompare(old_ps, new_ps)
+
+ old_review = build_review_number(review, old_ps)
+ new_review = build_review_number(review, new_ps)
+
+ old_branch = fetch_review(old_review, branch, remote)
+ checkout_review(old_branch)
+
+ if rebase:
+ print('Rebasing %s' % old_branch)
+ rebase = rebase_changes(branch, remote, False)
+ if not rebase:
+ print('Skipping rebase because of conflicts')
+ run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
+
+ new_branch = fetch_review(new_review, branch, remote)
+ checkout_review(new_branch)
+
+ if rebase:
+ print('Rebasing also %s' % new_branch)
+ if not rebase_changes(branch, remote, False):
+ print("Rebasing of the new branch failed, "
+ "diff can be messed up (use -R to not rebase at all)!")
+ run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
+
+ subprocess.check_call(['git', 'difftool', old_branch])
+
+
+def finish_branch(target_branch):
+ local_branch = get_branch_name(target_branch)
+ if VERBOSE:
+ print("Switching back to '%s' and deleting '%s'" % (target_branch,
+ local_branch))
+ run_command_exc(CheckoutBackExistingBranchFailed,
+ "git", "checkout", target_branch)
+ print("Switched to branch '%s'" % target_branch)
+
+ run_command_exc(DeleteBranchFailed,
+ "git", "branch", "-D", local_branch)
+ print("Deleted branch '%s'" % local_branch)
+
+
+def convert_bool(one_or_zero):
+ "Return a bool on a one or zero string."
+ return one_or_zero in ["1", "true", "True"]
+
+
+def print_exit_message(status, needs_update):
+
+ if needs_update:
+ print("""
+***********************************************************
+A new version of git-review is available on PyPI. Please
+update your copy with:
+
+ pip install -U git-review
+
+to ensure proper behavior with gerrit. Thanks!
+***********************************************************
+""")
+ sys.exit(status)
+
+
+def main():
+ global _no_color_support
+ usage = "git review [OPTIONS] ... [BRANCH]"
+
+ import argparse
+
+ class DownloadFlag(argparse.Action):
+ """Additional option parsing: store value in 'dest', but
+ at the same time set one of the flag options to True
+ """
+ def __call__(self, parser, namespace, values, option_string=None):
+ setattr(namespace, self.dest, values)
+ setattr(namespace, self.const, True)
+
+ parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT)
+
+ parser.add_argument("-t", "--topic", dest="topic",
+ help="Topic to submit branch to")
+ parser.add_argument("-D", "--draft", dest="draft", action="store_true",
+ help="Submit review as a draft")
+ parser.add_argument("-c", "--compatible", dest="compatible",
+ action="store_true",
+ help="Push change to refs/for/* for compatibility "
+ "with Gerrit versions < 2.3. Ignored if "
+ "-D/--draft is used.")
+ parser.add_argument("-n", "--dry-run", dest="dry", action="store_true",
+ help="Don't actually submit the branch for review")
+ parser.add_argument("-i", "--new-changeid", dest="regenerate",
+ action="store_true",
+ help="Regenerate Change-id before submitting")
+ parser.add_argument("-r", "--remote", dest="remote",
+ help="git remote to use for gerrit")
+ parser.add_argument("-R", "--no-rebase", dest="rebase",
+ action="store_false",
+ help="Don't rebase changes before submitting.")
+ parser.add_argument("-F", "--force-rebase", dest="force_rebase",
+ action="store_true",
+ help="Force rebase even when not needed.")
+ parser.add_argument("-B", "--no-color", dest="no_color_support",
+ action="store_true",
+ help="No color support.")
+
+
+ fetch = parser.add_mutually_exclusive_group()
+ fetch.set_defaults(download=False, compare=False, cherrypickcommit=False,
+ cherrypickindicate=False, cherrypickonly=False)
+ fetch.add_argument("-d", "--download", dest="changeidentifier",
+ action=DownloadFlag, metavar="CHANGE",
+ const="download",
+ help="Download the contents of an existing gerrit "
+ "review into a branch")
+ fetch.add_argument("-x", "--cherrypick", dest="changeidentifier",
+ action=DownloadFlag, metavar="CHANGE",
+ const="cherrypickcommit",
+ help="Apply the contents of an existing gerrit "
+ "review onto the current branch and commit "
+ "(cherry pick; not recommended in most "
+ "situations)")
+ fetch.add_argument("-X", "--cherrypickindicate", dest="changeidentifier",
+ action=DownloadFlag, metavar="CHANGE",
+ const="cherrypickindicate",
+ help="Apply the contents of an existing gerrit "
+ "review onto the current branch and commit, "
+ "indicating its origin")
+ fetch.add_argument("-N", "--cherrypickonly", dest="changeidentifier",
+ action=DownloadFlag, metavar="CHANGE",
+ const="cherrypickonly",
+ help="Apply the contents of an existing gerrit "
+ "review to the working directory and prepare "
+ "for commit")
+ fetch.add_argument("-m", "--compare", dest="changeidentifier",
+ action=DownloadFlag, metavar="CHANGE,PS[-NEW_PS]",
+ const="compare",
+ help="Download specified and latest (or NEW_PS) "
+ "patchsets of an existing gerrit review into "
+ "a branches, rebase on master "
+ "(skipped on conflicts or when -R is specified) "
+ "and show their differences")
+
+ parser.add_argument("-u", "--update", dest="update", action="store_true",
+ help="Force updates from remote locations")
+ parser.add_argument("-s", "--setup", dest="setup", action="store_true",
+ help="Just run the repo setup commands but don't "
+ "submit anything")
+ parser.add_argument("-f", "--finish", dest="finish", action="store_true",
+ help="Close down this branch and switch back to "
+ "master on successful submission")
+ parser.add_argument("-l", "--list", dest="list", action="store_true",
+ help="List available reviews for the current project")
+ parser.add_argument("-y", "--yes", dest="yes", action="store_true",
+ help="Indicate that you do, in fact, understand if "
+ "you are submitting more than one patch")
+ parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",
+ help="Output more information about what's going on")
+ parser.add_argument("--no-custom-script", dest="custom_script",
+ action="store_false", default=True,
+ help="Do not run custom scripts.")
+ parser.add_argument("--license", dest="license", action="store_true",
+ help="Print the license and exit")
+ parser.add_argument("--version", action="version",
+ version='%s version %s' %
+ (os.path.split(sys.argv[0])[-1], version))
+ parser.add_argument("branch", nargs="?")
+
+ try:
+ (top_dir, git_dir) = git_directories()
+ except GitDirectoriesException:
+ if sys.argv[1:] in ([], ['-h'], ['--help']):
+ parser.print_help()
+ sys.exit(1)
+ raise
+
+# config = get_config(os.path.join(top_dir, ".gitreview"))
+ config = DEFAULTS.copy()
+ cur_proj = run_command("git remote -v")
+ import re
+ m = re.search('(?<=29418\/)(\S+)', cur_proj)
+ config['project'] = m.group(0)
+
+
+ defaultrebase = convert_bool(
+ git_config_get_value("gitreview", "rebase",
+ default=str(config['defaultrebase'])))
+ parser.set_defaults(dry=False,
+ draft=False,
+ rebase=defaultrebase,
+ verbose=False,
+ update=False,
+ setup=False,
+ list=False,
+ yes=False,
+ branch=config['defaultbranch'],
+ remote=config['defaultremote'])
+ options = parser.parse_args()
+
+ if options.license:
+ print(COPYRIGHT)
+ sys.exit(0)
+
+ branch = options.branch
+ global VERBOSE
+ global UPDATE
+ VERBOSE = options.verbose
+ UPDATE = options.update
+ remote = options.remote
+ yes = options.yes
+ status = 0
+
+ needs_update = latest_is_newer()
+ check_remote(branch, remote,
+ config['hostname'], config['port'], config['project'])
+
+ if options.no_color_support:
+ _no_color_support = True
+
+ if options.changeidentifier:
+ if options.compare:
+ compare_review(options.changeidentifier,
+ branch, remote, options.rebase)
+ return
+ local_branch = fetch_review(options.changeidentifier, branch, remote)
+ if options.download:
+ checkout_review(local_branch)
+ else:
+ if options.cherrypickcommit:
+ cherrypick_review()
+ elif options.cherrypickonly:
+ cherrypick_review("-n")
+ if options.cherrypickindicate:
+ cherrypick_review("-x")
+ return
+ elif options.list:
+ list_reviews(remote)
+ return
+
+ if options.custom_script:
+ run_custom_script("pre")
+
+ hook_file = os.path.join(git_dir, "hooks", "commit-msg")
+ have_hook = os.path.exists(hook_file) and os.access(hook_file, os.X_OK)
+
+ if not have_hook:
+ set_hooks_commit_msg(remote, hook_file)
+
+ if options.setup:
+ if options.finish and not options.dry:
+ finish_branch(branch)
+ return
+
+ if options.rebase:
+ if not rebase_changes(branch, remote):
+ print_exit_message(1, needs_update)
+ if not options.force_rebase and not undo_rebase():
+ print_exit_message(1, needs_update)
+ assert_one_change(remote, branch, yes, have_hook)
+
+ ref = "publish"
+
+ if options.draft:
+ ref = "drafts"
+ if options.custom_script:
+ run_custom_script("draft")
+ elif options.compatible:
+ ref = "for"
+
+ cmd = "git push %s HEAD:refs/%s/%s" % (remote, ref, branch)
+ topic = options.topic or get_topic(branch)
+ if topic != branch:
+ cmd += "/%s" % topic
+ if options.regenerate:
+ print("Amending the commit to regenerate the change id\n")
+ regenerate_cmd = "git commit --amend"
+ if options.dry:
+ print("\tGIT_EDITOR=\"sed -i -e '/^Change-Id:/d'\" %s\n" %
+ regenerate_cmd)
+ else:
+ run_command(regenerate_cmd,
+ GIT_EDITOR="sed -i -e "
+ "'/^Change-Id:/d'")
+
+ if options.dry:
+ print("Please use the following command "
+ "to send your commits to review:\n")
+ print("\t%s\n" % cmd)
+ else:
+ (status, output) = run_command_status(cmd)
+ print(output)
+
+ if options.finish and not options.dry and status == 0:
+ finish_branch(branch)
+ return
+
+ if options.custom_script:
+ run_custom_script("post")
+ print_exit_message(status, needs_update)
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except GitReviewException as e:
+ print(e)
+ sys.exit(e.EXIT_CODE)