#!/usr/bin/env python # Copyright 2011 OpenStack, LLC. # All Rights Reserved. # # 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 commands import optparse import urllib import json from distutils.version import StrictVersion from urlparse import urlparse import ConfigParser import os import sys import time import re version = "1.2" VERBOSE = False UPDATE = False CONFIGDIR = os.path.expanduser("~/.config/git-review") PYPI_URL = "http://pypi.python.org/pypi/git-review/json" PYPI_CACHE_TIME = 60 * 60 * 24 # 24 hours def run_command(cmd, status=False): if VERBOSE: print "Running:", cmd stat, out = commands.getstatusoutput(cmd) if status: return (stat, out) return out def run_command_status(cmd): return run_command(cmd, True) 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(urllib.urlopen(PYPI_URL))['info']['version'] except: 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. """ 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 = StrictVersion(version_file.read()) if latest_version > StrictVersion(version): return True return False def set_hooks_commit_msg(): """ Install the commit message hook if needed. """ top_dir = run_command('git rev-parse --show-toplevel') target_file = os.path.join(top_dir, ".git/hooks/commit-msg") if os.path.exists(target_file) and os.access(target_file, os.X_OK): return (hostname, team, username, port, project_name) = \ parse_git_show("gerrit", "Push") source_location = "https://%s/tools/hooks/commit-msg" % hostname if not os.path.exists(target_file) or UPDATE: if VERBOSE: print "Fetching source_location: ", source_location commit_msg = urllib.urlretrieve(source_location, 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) run_command("GIT_EDITOR=true git commit --amend") def add_remote(username, hostname, port, project): """ Adds a gerrit remote. """ if username is None: username = os.getenv("USERNAME") if username is None: username = os.getenv("USER") if username is None: username = raw_input("Enter your gerrit username: ") if port is None: port = 29418 remote_url = "ssh://%s@%s:%s/%s" % (username, hostname, port, project) if VERBOSE: print "No remote set, testing %s" % remote_url ssh_cmd = "ssh -p%s -o StrictHostKeyChecking=no %s@%s gerrit ls-projects" cmd = ssh_cmd % (port, username, hostname) (status, ssh_outout) = run_command_status(cmd) if status == 0: if VERBOSE: print "%s@%s:%s worked." % (username, hostname, port) print "Creating a git remote called gerrit that maps to:" print "\t%s" % remote_url cmd = "git remote add -f gerrit %s" % remote_url (status, remote_output) = run_command_status(cmd) if status != 0: raise Exception("Error running %s" % cmd) def split_hostname(fetch_url): parsed_url = urlparse(fetch_url) username = None hostname = parsed_url.netloc port = 22 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": return (username, hostname, port) else: return (None, hostname, None) 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 = ":".join(line.split(":")[1:]).strip() project_name = fetch_url.split("/")[-1] if project_name.endswith(".git"): project_name = project_name[:-4] hostname = None team = None username = None port = None if VERBOSE: print "Found origin %s URL:" % verb, fetch_url # Special-case git@github urls - the rest can be parsed with urlparse if fetch_url.startswith("git@github.com"): hostname = "github.com" else: (username, hostname, port) = split_hostname(fetch_url) if hostname == "github.com": team = fetch_url.split("/")[-2] if team.startswith("git@github.com"): team = team.split(':')[1] return (hostname, team, username, port, project_name) def check_remote(remote): """Check that a Gerrit Git remote repo exists, if not, set one.""" if remote in run_command("git remote").split("\n"): for current_remote in run_command("git branch -a").split("\n"): if current_remote.strip() == "remotes/%s/master" % (remote) \ 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" output = run_command("git remote update %s" % remote) if VERBOSE: print output return # Check for a .gitreview at the top of the repo with the gerrit location top_dir = run_command('git rev-parse --show-toplevel') target_file = os.path.join(top_dir, ".gitreview") if os.path.exists(target_file): config = ConfigParser.ConfigParser(dict(port='29418')) config.read(target_file) hostname = config.get("gerrit", "host") port = config.get("gerrit", "port") project = config.get("gerrit", "project") username = None else: print "No '.gitreview' file found in this repository." print "We don't know where your gerrit is. Please manually create " print "a remote named gerrit and try again." sys.exit(1) # Gerrit remote not present, try to add it try: add_remote(username, hostname, port, project) except: print sys.exc_info()[2] print "We don't know where your gerrit is. Please manually create " print "a remote named gerrit and try again." raise def rebase_changes(branch, remote): remote_branch = "remotes/%s/%s" % (remote, branch) 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 cmd = "GIT_EDITOR=true git rebase -i %s" % remote_branch (status, output) = run_command_status(cmd) if status != 0: print "Errors running %s" % cmd print output return False return True def assert_diverge(branch, remote): cmd = "git diff %s/%s..HEAD" % (remote, branch) (status, output) = run_command_status(cmd) if len(output) == 0: print "No changes between HEAD and %s/%s." % (remote, branch) print "Submitting for review would be pointless." sys.exit(1) if status != 0: print "Had trouble running %s" % cmd sys.exit(1) def get_topic(): branch_name = None for branch in run_command("git branch").split("\n"): if branch.startswith('*'): branch_name = branch.split()[1].strip() branch_parts = branch_name.split("/") if len(branch_parts) >= 3 and branch_parts[0] == "review": return "/".join(branch_parts[2:]) log_output = run_command("git show --format='%s %b'") bug_re = r'\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)' match = re.search(bug_re, log_output) if match is not None: return "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 "bp/%s" % match.group(2) return branch_name def download_review(review): (hostname, team, username, port, project_name) = \ parse_git_show("gerrit", "Push") ssh_cmds = ["ssh"] if port is not None: ssh_cmds.extend(["-p", port]) if username is not None: ssh_cmds.extend(["-l", username]) ssh_cmd = " ".join(ssh_cmds) query_string = "--format=JSON --current-patch-set change:%s" % review review_info = None (status, output) = run_command_status("%s %s gerrit query %s" % (ssh_cmd, hostname, query_string)) if status != 0: print "Could not fetch review information from gerrit" print output return status try: review_info = json.loads(output.split("\n")[0]) except: if VERBOSE: print output print "Could not find a gerrit review with id: %s" % review return 1 topic = review_info['topic'] if topic == "master": topic = review author = re.sub('\W+', '_', review_info['owner']['name']).lower() branch_name = "review/%s/%s" % (author, topic) revision = review_info['currentPatchSet']['revision'] refspec = review_info['currentPatchSet']['ref'] print "Downloading %s from gerrit into %s" % (refspec, branch_name) checkout_cmd = "git checkout -b %s remotes/gerrit/master" % branch_name (status, output) = run_command_status(checkout_cmd) if status != 0: if output.endswith("already exists"): print "Branch already exists - reusing" checkout_cmd = "git checkout %s" % branch_name (status, output) = run_command_status(checkout_cmd) if status != 0: print output return status else: print output return status (status, output) = run_command_status("git pull gerrit %s" % refspec) if status != 0: print output return status (status, output) = run_command_status("git reset --hard %s" % revision) if status != 0: print output return status return 0 def print_exit_message(status, needs_update): if needs_update: print """ *********************************************************** A new version of git-review is availble on PyPI. Please update your copy with: pip install -U git-review to ensure proper behavior with gerrit. Thanks! *********************************************************** """ sys.exit(status) def main(): usage = "git review [OPTIONS] ... [BRANCH]" parser = optparse.OptionParser(usage=usage) parser.add_option("-t", "--topic", dest="topic", help="Topic to submit branch to") parser.add_option("-n", "--dry-run", dest="dry", action="store_true", help="Don't actually submit the branch for review") parser.add_option("-r", "--remote", dest="remote", help="git remote to use for gerrit") parser.add_option("-R", "--no-rebase", dest="rebase", action="store_false", help="Don't rebase changes before submitting.") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", help="Output more information about what's going on") parser.add_option("-d", "--download", dest="download", help="Download the contents of an existing gerrit " "review into a branch") parser.add_option("-u", "--update", dest="update", action="store_true", help="Force updates from remote locations") parser.add_option("-s", "--setup", dest="setup", action="store_true", help="Just run the repo setup commands but don't " "submit anything") parser.set_defaults(dry=False, rebase=True, verbose=False, update=False, setup=False, remote="gerrit") branch = "master" (options, args) = parser.parse_args() if len(args) > 0: branch = args[0] global VERBOSE global UPDATE VERBOSE = options.verbose UPDATE = options.update remote = options.remote status = 0 needs_update = latest_is_newer() check_remote(remote) if options.download is not None: print_exit_message(download_review(options.download), needs_update) else: topic = options.topic if topic is None: topic = get_topic() if VERBOSE: print "Found topic '%s' from parsing changes." % topic set_hooks_commit_msg() if not options.setup: if options.rebase: if not rebase_changes(branch, remote): print_exit_message(1, needs_update) assert_diverge(branch, remote) cmd = "git push %s HEAD:refs/for/%s/%s" % (remote, branch, topic) 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 print_exit_message(status, needs_update) if __name__ == "__main__": main()