Hi All.
I was trying to integrate SVN(on Windows) with ReviewBoard 2.5.9.(on Ubuntu)
Intgration went fine and I was able to add SVN Repo through Admin portal of
ReviewBoard.
Next step was to add precommit hook on SVN, so that any commit on SVN
should either be using existing review ID or create a new review.
I got precommit_hook.py and postreview.py python scripts to achieve this
task(when I searched on google).
but when I am executing these scripts I found that I am unable to do login
to review board while providing correct Id and pwd.
/api/json/accounts/login/ was the url that script was trying to hit and
receiving "not reachable".
When I tried with Postman(open free ware tool to test REST API) to hit
http://<MyReviewBoardIP>/api/json/accounts/login with HTTP_POST then also I
got error saying Object does not exist code 100 .
Where is the issue ?
did I miss any install steps for WEB API ?
are these APIs old ?
Can I get latest verison of precommit_hook.py and postreview.py scripts ?
attaching the ones that I am using.
Thanks,
Manish Trivedi
--
Supercharge your Review Board with Power Pack:
https://www.reviewboard.org/powerpack/
Want us to host Review Board for you? Check out RBCommons:
https://rbcommons.com/
Happy user? Let us know! https://www.reviewboard.org/users/
---
You received this message because you are subscribed to the Google Groups
"reviewboard" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.
#!/usr/bin/env python
import cookielib
import mimetools
import os
import getpass
import re
import simplejson
import socket
import subprocess
import sys
import urllib2
from optparse import OptionParser
from tempfile import mkstemp
from urlparse import urljoin, urlparse
###
# Default configuration -- user-settable variables follow.
###
# The following settings usually aren't needed, but if your Review
# Board crew has specific preferences and doesn't want to express
# them with command line switches, set them here and you're done.
# In particular, setting the REVIEWBOARD_URL variable will allow
# you to make it easy for people to submit reviews regardless of
# their SCM setup.
# Reviewboard URL.
#
# Set this if you wish to hard-code a default server to always use.
# It's generally recommended to set this on your SCM repository instead
# for those that support it (currently only SVN and Git).
#
# For example, on SVN:
# $ svn propset reviewboard:url http://reviewboard.example.com .
#
# Or with Git:
# $ git config reviewboard.url http://reviewboard.example.com
#
# If this is not a concern, setting the value here will let you get started
# quickly.
REVIEWBOARD_URL = "http://10.112.91.84"
# Default submission arguments. These are all optional; run this
# script with --help for descriptions of each argument.
TARGET_GROUPS = None
TARGET_PEOPLE = None
SUBMIT_AS = None
PUBLISH = False
OPEN_BROWSER = False
# Debugging. For development...
DEBUG = False
###
# End user-settable variables.
###
VERSION = "0.7"
user_config = None
tempfiles = []
options = None
class APIError(Exception):
pass
class RepositoryInfo:
"""
A representation of a source code repository.
"""
def __init__(self, path=None, base_path=None, supports_changesets=False):
self.path = path
self.base_path = base_path
self.supports_changesets = supports_changesets
def __str__(self):
return "Path: %s, Base path: %s, Supports changesets: %s" % \
(self.path, self.base_path, self.supports_changesets)
class ReviewBoardHTTPPasswordMgr(urllib2.HTTPPasswordMgr):
"""
Adds HTTP authentication support for URLs.
Python 2.4's password manager has a bug in http authentication when the
target server uses a non-standard port. This works around that bug on
Python 2.4 installs. This also allows post-review to prompt for passwords
in a consistent way.
See: http://bugs.python.org/issue974757
"""
def __init__(self, reviewboard_url):
self.passwd = {}
self.rb_url = reviewboard_url
self.rb_user = None
self.rb_pass = None
def find_user_password(self, realm, uri):
if uri.startswith(self.rb_url):
if self.rb_user is None or self.rb_pass is None:
print "==> HTTP Authentication Required"
print 'Enter username and password for "%s" at %s' % \
(realm, urlparse(uri)[1])
self.rb_user = raw_input('Username: ')
self.rb_pass = getpass.getpass('Password: ')
return self.rb_user, self.rb_pass
else:
# If this is an auth request for some other domain (since HTTP
# handlers are global), fall back to standard password management.
return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
class ReviewBoardServer:
"""
An instance of a Review Board server.
"""
def __init__(self, url, info, cookie_file):
self.url = url
self.info = info
self.cookie_file = cookie_file
self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
# Set up the HTTP libraries to support all of the features we need.
cookie_handler = urllib2.HTTPCookieProcessor(self.cookie_jar)
password_mgr = ReviewBoardHTTPPasswordMgr(self.url)
auth_handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(cookie_handler, auth_handler)
opener.addheaders = [('User-agent', 'post-review/' + VERSION)]
urllib2.install_opener(opener)
def login(self, username=None, password=None):
"""
Logs in to a Review Board server, prompting the user for login
information if needed.
"""
if self.has_valid_cookie():
return
print "==> Review Board Login Required"
print "Enter username and password for Review Board at %s" % self.url
if not username:
username = raw_input('Username: ')
if not password:
password = getpass.getpass('Password: ')
debug('Logging in with username "%s"' % username)
try:
self.api_post('/api/json/accounts/login/', {
'username': username,
'password': password,
})
except APIError, e:
rsp, = e.args
die("Unable to log in: %s (%s)" % (rsp["err"]["msg"],
rsp["err"]["code"]))
debug("Logged in.")
def has_valid_cookie(self):
"""
Load the user's cookie file and see if they have a valid
'sessionid' cookie for the current Review Board server. Returns
true if so and false otherwise.
"""
try:
parsed_url = urlparse(self.url)
host = parsed_url[1]
path = parsed_url[2] or '/'
# Cookie files don't store port numbers, unfortunately, so
# get rid of the port number if it's present.
host = host.split(":")[0]
debug("Looking for '%s %s' cookie in %s" % \
(host, path, self.cookie_file))
self.cookie_jar.load(self.cookie_file, ignore_expires=True)
try:
cookie = self.cookie_jar._cookies[host][path]['rbsessionid']
if not cookie.is_expired():
debug("Loaded valid cookie -- no login required")
return True
debug("Cookie file loaded, but cookie has expired")
except KeyError:
debug("Cookie file loaded, but no cookie for this server")
except IOError, error:
debug("Couldn't load cookie file: %s" % error)
return False
def new_review_request(self, changenum, submit_as=None):
"""
Creates a review request on a Review Board server, updating an
existing one if the changeset number already exists.
If submit_as is provided, the specified user name will be recorded as
the submitter of the review request (given that the logged in user has
the appropriate permissions).
"""
try:
debug("Attempting to create review request for %s" % changenum)
data = { 'repository_path': self.info.path }
if changenum:
data['changenum'] = changenum
if submit_as:
debug("Submitting the review request as %s" % submit_as)
data['submit_as'] = submit_as
rsp = self.api_post('/api/json/reviewrequests/new/', data)
except APIError, e:
rsp, = e.args
if not options.diff_only:
if rsp['err']['code'] == 204: # Change number in use
debug("Review request already exists. Updating it...")
rsp = self.api_post(
'/api/json/reviewrequests/%s/update_from_changenum/' %
rsp['review_request']['id'])
else:
raise e
debug("Review request created")
return rsp['review_request']
def set_review_request_field(self, review_request, field, value):
"""
Sets a field in a review request to the specified value.
"""
rid = review_request['id']
debug("Attempting to set field '%s' to '%s' for review request '%s'" %
(field, value, rid))
self.api_post('/api/json/reviewrequests/%s/draft/set/' % rid, {
field: value,
})
def get_review_request(self, rid):
"""
Returns the review request with the specified ID.
"""
rsp = self.api_get('/api/json/reviewrequests/%s/' % rid)
return rsp['review_request']
def save_draft(self, review_request):
"""
Saves a draft of a review request.
"""
self.api_post("/api/json/reviewrequests/%s/draft/save/" %
review_request['id'])
debug("Review request draft saved")
def upload_diff(self, review_request, diff_content):
"""
Uploads a diff to a Review Board server.
"""
debug("Uploading diff, size: %d" % (len(diff_content),))
fields = {}
if self.info.base_path:
fields['basedir'] = self.info.base_path
self.api_post('/api/json/reviewrequests/%s/diff/new/' %
review_request['id'], fields,
{'path': {'filename': 'diff',
'content': diff_content}})
def publish(self, review_request):
"""
Publishes a review request.
"""
debug("Publishing")
self.http_post(path='/r/%s/publish/' % review_request['id'],
fields = {})
def process_json(self, data):
"""
Loads in a JSON file and returns the data if successful. On failure,
APIError is raised.
"""
rsp = simplejson.loads(data)
if rsp['stat'] == 'fail':
raise APIError, rsp
return rsp
def http_get(self, path):
"""
Performs an HTTP GET on the specified path, storing any cookies that
were set.
"""
debug('HTTP GETting %s' % path)
url = self._make_url(path)
try:
rsp = urllib2.urlopen(url).read()
self.cookie_jar.save(self.cookie_file)
return rsp
except urllib2.HTTPError, e:
print "Unable to access %s (%s). The host path may be invalid" % \
(url, e.code)
try:
debug(e.read())
except AttributeError:
pass
die()
def _make_url(self, path):
"""Given a path on the server returns a full http:// style url"""
url = urljoin(self.url, path)
if not url.startswith('http'):
url = 'http://%s' % url
return url
def api_get(self, path):
"""
Performs an API call using HTTP GET at the specified path.
"""
return self.process_json(self.http_get(path))
def http_post(self, path, fields, files=None):
"""
Performs an HTTP POST on the specified path, storing any cookies that
were set.
"""
if fields:
debug_fields = fields.copy()
else:
debug_fields = {}
if 'password' in debug_fields:
debug_fields["password"] = "**************"
url = self._make_url(path)
debug('HTTP POSTing to %s: %s' % (url, debug_fields))
content_type, body = self._encode_multipart_formdata(fields, files)
headers = {
'Content-Type': content_type,
'Content-Length': str(len(body))
}
try:
r = urllib2.Request(url, body, headers)
data = urllib2.urlopen(r).read()
self.cookie_jar.save(self.cookie_file)
return data
except urllib2.URLError, e:
try:
debug(e.read())
except AttributeError:
pass
die("Unable to access %s. The host path may be invalid\n%s" % \
(url, e))
except urllib2.HTTPError, e:
die("Unable to access %s (%s). The host path may be invalid\n%s" % \
(url, e.code, e.read()))
def api_post(self, path, fields=None, files=None):
"""
Performs an API call using HTTP POST at the specified path.
"""
return self.process_json(self.http_post(path, fields, files))
def _encode_multipart_formdata(self, fields, files):
"""
Encodes data for use in an HTTP POST.
"""
BOUNDARY = mimetools.choose_boundary()
content = ""
fields = fields or {}
files = files or {}
for key in fields:
content += "--" + BOUNDARY + "\r\n"
content += "Content-Disposition: form-data; name=\"%s\"\r\n" % key
content += "\r\n"
content += fields[key] + "\r\n"
for key in files:
filename = files[key]['filename']
value = files[key]['content']
content += "--" + BOUNDARY + "\r\n"
content += "Content-Disposition: form-data; name=\"%s\"; " % key
content += "filename=\"%s\"\r\n" % filename
content += "\r\n"
content += value + "\r\n"
content += "--" + BOUNDARY + "--\r\n"
content += "\r\n"
content_type = "multipart/form-data; boundary=%s" % BOUNDARY
return content_type, content
class SCMClient(object):
"""
A base representation of an SCM tool for fetching repository information
and generating diffs.
"""
def get_repository_info(self):
return None
def scan_for_server(self, repository_info):
"""
Scans the current directory on up to find a .reviewboard file
containing the server path.
"""
server_url = self._get_server_from_config(user_config, repository_info)
if server_url:
return server_url
for path in walk_parents(os.getcwd()):
filename = os.path.join(path, ".reviewboardrc")
if os.path.exists(filename):
config = load_config_file(filename)
server_url = self._get_server_from_config(config,
repository_info)
if server_url:
return server_url
return None
def diff(self, args):
return None
def diff_between_revisions(self, revision_range):
return None
def add_options(self, parser):
"""
Adds options to an OptionParser.
"""
pass
def _get_server_from_config(self, config, repository_info):
if 'REVIEWBOARD_URL' in config:
return config['REVIEWBOARD_URL']
elif 'TREES' in config:
trees = config['TREES']
if not isinstance(trees, dict):
die("Warning: 'TREES' in config file is not a dict!")
if repository_info.path in trees and \
'REVIEWBOARD_URL' in trees[repository_info.path]:
return trees[repository_info.path]['REVIEWBOARD_URL']
return None
class SVNClient(SCMClient):
"""
A wrapper around the svn Subversion tool that fetches repository
information and generates compatible diffs.
"""
def get_repository_info(self):
if not check_install('svn help'):
return None
data = execute('svn info', ignore_errors=True)
m = re.search(r'^Repository Root: (.+)$', data, re.M)
if not m:
return None
path = m.group(1)
m = re.search(r'^URL: (.+)$', data, re.M)
if not m:
return None
base_path = m.group(1)[len(path):]
return RepositoryInfo(path=path, base_path=base_path)
def scan_for_server(self, repository_info):
# Scan first for dot files, since it's faster and will cover the
# user's $HOME/.reviewboardrc
server_url = super(SVNClient, self).scan_for_server(repository_info)
if server_url:
return server_url
return self.scan_for_server_property(repository_info)
def scan_for_server_property(self, repository_info):
def get_url_prop(path):
url = execute("svn propget reviewboard:url %s" % path).strip()
return url or None
for path in walk_parents(os.getcwd()):
if not os.path.exists(os.path.join(path, ".svn")):
break
prop = get_url_prop(path)
if prop:
return prop
return get_url_prop(repository_info.path)
def diff(self, files):
"""
Performs a diff across all modified files in a Subversion repository.
"""
return execute('svn diff --diff-cmd=diff %s' % ' '.join(files))
def diff_between_revisions(self, revision_range):
"""
Performs a diff between 2 revisions of a Subversion repository.
"""
return execute('svn diff --diff-cmd=diff -r %s' % revision_range)
class PerforceClient(SCMClient):
"""
A wrapper around the p4 Perforce tool that fetches repository information
and generates compatible diffs.
"""
def getPendingCLs(self, expensive=None):
if not check_install('p4 help'):
print "P4 is not correctly installed"
return [] # p4 isn't installed, so we don't have any CLs to view
cmd = 'p4 changes -u %s -s pending' % self.username
f = os.popen(cmd, 'r')
data = f.readlines()
f.close()
cls = []
for a in data:
info = re.search("Change (.*) on (.*) \*pending\* '(.*)'", a)
if not info:
continue
num = info.group(1)
desc = info.group(3)
file_zone = False
full = ""
file = ""
if expensive:
cmd = 'p4 describe -s %s' % num
f = os.popen(cmd, 'r')
descrInfo = f.readlines()
f.close()
lines = 0
for l in descrInfo:
l = l.strip()
if "Affected files ..." in l:
file_zone = True
if file_zone:
file += l + "\n"
else:
full += l + "\n"
desc = full.split('\n')[2]
cls.append((num, desc, full, file))
return cls
def get_repository_info(self):
if not check_install('p4 help'):
return None
data = execute('p4 info', ignore_errors=True)
m = re.search(r'^User name: (.+)$', data, re.M)
if m:
self.username = m.group(1).strip()
m = re.search(r'^Server address: (.+)$', data, re.M)
if not m:
return None
repository_path = m.group(1).strip()
try:
hostname, port = repository_path.split(":")
info = socket.gethostbyaddr(hostname)
repository_path = "%s:%s" % (info[0], port)
except (socket.gaierror, socket.herror):
pass
return RepositoryInfo(path=repository_path, supports_changesets=True)
def diff(self, args):
"""
Goes through the hard work of generating a diff on Perforce in order
to take into account adds/deletes and to provide the necessary
revision information.
"""
if len(args) != 1:
print "Specify the change number of a pending changeset"
sys.exit(1)
changenum = args[0]
cl_is_pending = False
try:
changenum = int(changenum)
except ValueError:
die("You must enter a valid change number")
debug("Generating diff for changenum %s" % changenum)
description = execute('p4 describe -s %d' % changenum).splitlines()
if '*pending*' in description[0]:
cl_is_pending = True
# Get the file list
for line_num, line in enumerate(description):
if 'Affected files ...' in line:
break
else:
# Got to the end of all the description lines and didn't find
# what we were looking for.
die("Couldn't find any affected files for this change.")
description = description[line_num+2:]
cwd = os.getcwd()
diff_lines = []
empty_filename = make_tempfile()
tmp_diff_from_filename = make_tempfile()
tmp_diff_to_filename = make_tempfile()
for line in description:
line = line.strip()
if not line:
continue
m = re.search(r'\.\.\. ([^#]+)#(\d+) (add|edit|delete|integrate|branch)', line)
if not m:
die("Unsupported line from p4 opened: %s" % line)
depot_path = m.group(1)
base_revision = int(m.group(2))
if not cl_is_pending:
# If the changelist is pending our base revision is the one that's
# currently in the depot. If we're not pending the base revision is
# actually the revision prior to this one
base_revision -= 1
changetype = m.group(3)
debug('Processing %s of %s' % (changetype, depot_path))
local_name = m.group(1)
old_file = new_file = empty_filename
old_depot_path = new_depot_path = None
changetype_short = None
if changetype == 'edit' or changetype == 'integrate':
# A big assumption
new_revision = base_revision + 1
# We have an old file, get p4 to take this old version from the
# depot and put it into a plain old temp file for us
old_depot_path = "%s#%s" % (depot_path, base_revision)
self._write_file(old_depot_path, tmp_diff_from_filename)
old_file = tmp_diff_from_filename
# Also print out the new file into a tmpfile
if cl_is_pending:
new_file = self._depot_to_local(depot_path)
else:
new_depot_path = "%s#%s" %(depot_path, new_revision)
self._write_file(new_depot_path, tmp_diff_to_filename)
new_file = tmp_diff_to_filename
changetype_short = "M"
elif changetype == 'add' or changetype == 'branch':
# We have a new file, get p4 to put this new file into a pretty
# temp file for us. No old file to worry about here.
if cl_is_pending:
new_file = self._depot_to_local(depot_path)
else:
self._write_file(depot_path, tmp_diff_to_filename)
new_file = tmp_diff_to_filename
changetype_short = "A"
elif changetype == 'delete':
# We've deleted a file, get p4 to put the deleted file into a temp
# file for us. The new file remains the empty file.
old_depot_path = "%s#%s" % (depot_path, base_revision)
self._write_file(old_depot_path, tmp_diff_from_filename)
old_file = tmp_diff_from_filename
changetype_short = "D"
else:
die("Unknown change type '%s' for %s" % (changetype, depot_path))
diff_cmd = 'diff -urNp "%s" "%s"' % (old_file, new_file)
# Diff returns "1" if differences were found.
dl = execute(diff_cmd, extra_ignore_errors=(1,)).splitlines(True)
if local_name.startswith(cwd):
local_path = local_name[len(cwd) + 1:]
else:
local_path = local_name
# Special handling for the output of the diff tool on binary files:
# diff outputs "Files a and b differ"
# and the code below expects the outptu to start with
# "Binary files "
if len(dl) == 1 and \
dl[0] == ('Files %s and %s differ'% (old_file, new_file)):
dl = ['Binary files %s and %s differ'% (old_file, new_file)]
if dl == [] or dl[0].startswith("Binary files "):
if dl == []:
print "Warning: %s in your changeset is unmodified" % \
local_path
dl.insert(0, "==== %s#%s ==%s== %s ====\n" % \
(depot_path, base_revision, changetype_short, local_path))
else:
m = re.search(r'(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)', dl[1])
if m:
timestamp = m.group(1)
else:
# Thu Sep 3 11:24:48 2007
m = re.search(r'(\w+)\s+(\w+)\s+(\d+)\s+(\d\d:\d\d:\d\d)\s+(\d\d\d\d)', dl[1])
if not m:
die("Unable to parse diff header: %s" % dl[1])
month_map = {
"Jan": "01",
"Feb": "02",
"Mar": "03",
"Apr": "04",
"May": "05",
"Jun": "06",
"Jul": "07",
"Aug": "08",
"Sep": "09",
"Oct": "10",
"Nov": "11",
"Dec": "12",
}
month = month_map[m.group(2)]
day = m.group(3)
timestamp = m.group(4)
year = m.group(5)
timestamp = "%s-%s-%s %s" % (year, month, day, timestamp)
dl[0] = "--- %s\t%s#%s\n" % (local_path, depot_path, base_revision)
dl[1] = "+++ %s\t%s\n" % (local_path, timestamp)
diff_lines += dl
os.unlink(empty_filename)
os.unlink(tmp_diff_from_filename)
os.unlink(tmp_diff_to_filename)
return ''.join(diff_lines)
def _write_file(self, depot_path, tmpfile):
"""
Grabs a file from Perforce and writes it to a temp file. We do this
wrather than telling p4 print to write it out in order to work around
a permissions bug on Windows.
"""
debug('Writing "%s" to "%s"' % (depot_path, tmpfile))
data = execute('p4 print -q "%s"' % depot_path)
f = open(tmpfile, "w")
f.write(data)
f.close()
def _depot_to_local(self, depot_path):
"""
Given a path in the depot return the path on the local filesystem to
the same file.
"""
# $ p4 where //user/bvanzant/main/testing
# //user/bvanzant/main/testing //bvanzant:test05:home/testing /home/bvanzant/home-versioned/testing
cmd = 'p4 where "%s"' % (depot_path,)
where_output = execute(cmd).splitlines()
# Take only the last line from the where command. If you have a
# multi-line view mapping with exclusions, Perforce will display
# the exclusions in order, with the last line showing the actual
# location.
last_line = where_output[-1]
# XXX: This breaks on filenames with spaces.
return last_line.split(' ')[2]
class MercurialClient(SCMClient):
"""
A wrapper around the hg Mercurial tool that fetches repository
information and generates compatible diffs.
"""
def get_repository_info(self):
if not check_install('hg --help'):
return None
data = execute('hg root', ignore_errors=True)
if data.startswith('abort:'):
# hg aborted => no mercurial repository here.
return None
# Elsewhere, hg root output give us the repository path.
# We save data here to use it as a fallback. See below
local_data = data.strip()
# We are going to search .hg/hgrc for the default path.
file_name = os.path.join(local_data,'.hg', 'hgrc')
if not os.path.exists(file_name):
return RepositoryInfo(path=local_data, base_path='/')
f = open(file_name)
data = f.read()
f.close()
m = re.search(r'^default\s+=\s+(.+)$', data, re.M)
if not m:
# Return the local path, if no default value is found.
return RepositoryInfo(path=local_data, base_path='/')
path = m.group(1).strip()
return RepositoryInfo(path=path, base_path='')
def diff(self, files):
"""
Performs a diff across all modified files in a Mercurial repository.
"""
return execute('hg diff %s' % ' '.join(files))
def diff_between_revisions(self, revision_range):
"""
Performs a diff between 2 revisions of a Mercurial repository.
"""
r1, r2 = revision_range.split(':')
return execute('hg diff -r %s -r %s' % (r1, r2))
class GitClient(SCMClient):
"""
A wrapper around git that fetches repository information and generates
compatible diffs. This will attempt to generate a diff suitable for the
remote repository, whether git, SVN or Perforce.
"""
def get_repository_info(self):
if not check_install('git --help'):
return None
git_dir = execute('git rev-parse --git-dir', ignore_errors=True).strip()
if git_dir.startswith("fatal:") or not os.path.isdir(git_dir):
return None
# We know we have something we can work with. Let's find out
# what it is. We'll try SVN first.
data = execute("git svn info", ignore_errors=True)
m = re.search(r'^Repository Root: (.+)$', data, re.M)
if m:
path = m.group(1)
m = re.search(r'^URL: (.+)$', data, re.M)
if m:
base_path = m.group(1)[len(path):]
self.type = "svn"
return RepositoryInfo(path=path, base_path=base_path)
# Okay, maybe Perforce.
# TODO
# Nope, it's git then.
# TODO
return None
def scan_for_server(self, repository_info):
# Scan first for dot files, since it's faster and will cover the
# user's $HOME/.reviewboardrc
# TODO: Maybe support a server per remote later? Is that useful?
url = execute("git config --get reviewboard.url").strip()
if url:
return url
if self.type == "svn":
# Try using the reviewboard:url property on the SVN repo, if it
# exists.
prop = SVNClient().scan_for_server_property(repository_info)
if prop:
return prop
return None
def diff(self, args):
"""
Performs a diff across all modified files in the branch.
"""
if len(args) == 0:
branch_against = "master"
elif len(args) == 1:
branch_against = args[0]
else:
print "No more than one branch can be supplied."
sys.exit(1)
diff_lines = execute("git diff --no-color --no-prefix -r -u %s.." %
branch_against,
split_lines=True)
if self.type == "svn":
return self.make_svn_diff(branch_against, diff_lines)
return None
def make_svn_diff(self, branch_against, diff_lines):
"""
Formats the output of git diff such that it's in a form that
svn diff would generate. This is needed so the SVNTool in Review
Board can properly parse this diff.
"""
rev = execute("git-svn find-rev %s" % branch_against).strip()
if not rev:
return None
diff_data = ""
filename = ""
revision = ""
newfile = False
for line in diff_lines:
if line.startswith("diff "):
# Grab the filename and then filter this out.
# This will be in the format of:
#
# diff --git a/path/to/file b/path/to/file
info = line.split(" ")
diff_data += "Index: %s\n" % info[2]
diff_data += "=" * 63
diff_data += "\n"
elif line.startswith("index "):
# Filter this out.
pass
elif line.strip() == "--- /dev/null":
# New file
newfile = True
elif line.startswith("--- "):
newfile = False
diff_data += "--- %s\t(revision %s)\n" % \
(line[4:].strip(), rev)
elif line.startswith("+++ "):
filename = line[4:].strip()
if newfile:
diff_data += "--- %s\t(revision 0)\n" % filename
diff_data += "+++ %s\t(revision 0)\n" % filename
else:
# We already printed the "--- " line.
diff_data += "+++ %s\t(working copy)\n" % filename
else:
diff_data += line
return diff_data
def diff_between_revisions(self, revision_range):
pass
def debug(s):
"""
Prints debugging information if post-review was run with --debug
"""
if options.debug:
print ">>> %s" % s
def make_tempfile():
"""
Creates a temporary file and returns the path. The path is stored
in an array for later cleanup.
"""
fd, tmpfile = mkstemp()
os.close(fd)
tempfiles.append(tmpfile)
return tmpfile
def check_install(command):
"""
Try executing an external command and return a boolean indicating whether
that command is installed or not. The 'command' argument should be
something that executes quickly, without hitting the network (for
instance, 'svn help' or 'git --version').
"""
try:
p = subprocess.Popen(command.split(' '),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
return True
except OSError:
return False
def execute(command, split_lines=False, ignore_errors=False,
extra_ignore_errors=()):
"""
Utility function to execute a command and return the output.
"""
if sys.platform.startswith('win'):
p = subprocess.Popen(command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True)
else:
p = subprocess.Popen(command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
close_fds=True)
if split_lines:
data = p.stdout.readlines()
else:
data = p.stdout.read()
rc = p.wait()
if rc and not ignore_errors and rc not in extra_ignore_errors:
die('Failed to execute command: %s\n%s' % (command, data))
return data
def die(msg=None):
"""
Cleanly exits the program with an error message. Erases all remaining
temporary files.
"""
for tmpfile in tempfiles:
try:
os.unlink(tmpfile)
except:
pass
if msg:
print msg
sys.exit(1)
def walk_parents(path):
"""
Walks up the tree to the root directory.
"""
while os.path.splitdrive(path)[1] != os.sep:
yield path
path = os.path.dirname(path)
def load_config_file(filename):
"""
Loads data from a config file.
"""
config = {
'TREES': {},
}
if os.path.exists(filename):
try:
execfile(filename, config)
except:
pass
return config
def tempt_fate(server, tool, changenum, diff_content=None, submit_as=None):
"""
Attempts to create a review request on a Review Board server and upload
a diff. On success, the review request path is displayed.
"""
try:
save_draft = False
if options.rid:
review_request = server.get_review_request(options.rid)
else:
review_request = server.new_review_request(changenum, submit_as)
if options.target_groups:
server.set_review_request_field(review_request, 'target_groups',
options.target_groups)
save_draft = True
if options.target_people:
server.set_review_request_field(review_request, 'target_people',
options.target_people)
save_draft = True
if options.summary:
server.set_review_request_field(review_request, 'summary',
options.summary)
save_draft = True
if options.description:
server.set_review_request_field(review_request, 'description',
options.description)
save_draft = True
if save_draft:
server.save_draft(review_request)
except APIError, e:
rsp, = e.args
if rsp['err']['code'] == 103: # Not logged in
server.login()
tempt_fate(server, tool, changenum, diff_content, submit_as)
return
if options.rid:
die("Error getting review request %s: %s (code %s)" % \
(options.rid, rsp['err']['msg'], rsp['err']['code']))
else:
die("Error creating review request: %s (code %s)" % \
(rsp['err']['msg'], rsp['err']['code']))
if not server.info.supports_changesets or not options.change_only:
try:
server.upload_diff(review_request, diff_content)
except APIError, e:
rsp, = e.args
print "Error uploading diff: %s (%s)" % (rsp['err']['msg'],
rsp['err']['code'])
debug(rsp)
die("Your review request still exists, but the diff is not " +
"attached.")
if options.publish:
server.publish(review_request)
review_url = '%s/%s/%s/' % (server.url, "r", review_request['id'])
if not review_url.startswith('http'):
review_url = 'http://%s' % review_url
print "Review request #%s posted." % (review_request['id'],)
print
print review_url
return review_url
def parse_options(tool, repository_info, args):
parser = OptionParser(usage="%prog [-pond] [-r review_id] [changenum]",
version="%prog " + VERSION)
parser.add_option("-p", "--publish",
dest="publish", action="store_true", default=PUBLISH,
help="publish the review request immediately after " +
"submitting")
parser.add_option("-r", "--review-request-id",
dest="rid", metavar="ID", default=None,
help="existing review request ID to update")
parser.add_option("-o", "--open",
dest="open_browser", action="store_true",
default=OPEN_BROWSER,
help="open a web browser to the review request page")
parser.add_option("-n", "--output-diff",
dest="output_diff_only", action="store_true",
default=False,
help="outputs a diff to the console and exits. " +
"Does not post")
parser.add_option("--server",
dest="server", default=REVIEWBOARD_URL,
metavar="SERVER",
help="specify a different Review Board server " +
"to use")
parser.add_option("--diff-only",
dest="diff_only", action="store_true", default=False,
help="uploads a new diff, but does not update " +
"info from changelist")
parser.add_option("--target-groups",
dest="target_groups", default=TARGET_GROUPS,
help="names of the groups who will perform " +
"the review")
parser.add_option("--target-people",
dest="target_people", default=TARGET_PEOPLE,
help="names of the people who will perform " +
"the review")
parser.add_option("--summary",
dest="summary", default=None,
help="summary of the review ")
parser.add_option("--username",
dest="username", default=None, metavar="USERNAME",
help="user name to be supplied to the reviewboard server")
parser.add_option("--password",
dest="password", default=None, metavar="PASSWORD",
help="password to be supplied to the reviewboard server")
parser.add_option("--description",
dest="description", default=None,
help="description of the review ")
parser.add_option("--revision-range",
dest="revision_range", default=None,
help="generate the diff for review based on given " +
"revision range")
parser.add_option("--submit-as",
dest="submit_as", default=SUBMIT_AS, metavar="USERNAME",
help="user name to be recorded as the author of the "
"review request, instead of the logged in user")
if repository_info:
if repository_info.supports_changesets:
parser.add_option("--change-only",
dest="change_only", action="store_true",
default=False,
help="updates info from changelist, but does " +
"not upload a new diff")
tool.add_options(parser)
parser.add_option("-d", "--debug",
action="store_true", dest="debug", default=DEBUG,
help="display debug output")
(globals()["options"], args) = parser.parse_args(args)
return args
def main(args):
if 'USERPROFILE' in os.environ:
homepath = os.path.join(os.environ["USERPROFILE"], "Local Settings",
"Application Data")
else:
homepath = os.environ["HOME"]
# Load the config and cookie files
globals()['user_config'] = \
load_config_file(os.path.join(homepath, ".reviewboardrc"))
cookie_file = os.path.join(homepath, ".post-review-cookies.txt")
# Try to find the SCM Client we're going to be working with
repository_info = None
tool = None
for tool in (SVNClient(), MercurialClient(), GitClient(), PerforceClient()):
repository_info = tool.get_repository_info()
if repository_info:
break
args = parse_options(tool, repository_info, args)
if not repository_info:
print "The current directory does not contain a checkout from a"
print "supported source code repository."
sys.exit(1)
debug("Repository info '%s'" % repository_info)
# Try to find a valid Review Board server to use.
if options.server:
server_url = options.server
else:
server_url = tool.scan_for_server(repository_info)
if not server_url:
print "Unable to find a Review Board server for this source code tree."
sys.exit(1)
server = ReviewBoardServer(server_url, repository_info, cookie_file)
if repository_info.supports_changesets:
if len(args) < 1:
print "You must include a change set number"
sys.exit(1)
changenum = args[0]
else:
changenum = None
if options.revision_range:
diff = tool.diff_between_revisions(options.revision_range)
else:
diff = tool.diff(args)
if options.output_diff_only:
print diff
sys.exit(0)
# Let's begin.
server.login()
review_url = tempt_fate(server, tool, changenum, diff_content=diff,
submit_as=options.submit_as)
# Load the review up in the browser if requested to:
if options.open_browser:
try:
import webbrowser
if 'open_new_tab' in dir(webbrowser):
# open_new_tab is only in python 2.5+
webbrowser.open_new_tab(review_url)
elif 'open_new' in dir(webbrowser):
webbrowser.open_new(review_url)
else:
os.system( 'start %s' % review_url )
except:
print 'Error opening review URL: %s' % review_url
if __name__ == "__main__":
main(sys.argv[1:])
#!/usr/bin/env python
# If you install this as a pre-commit hook in your Subversion repository, it
# will only let proceed commits that has a valid, approved, not submited
# reviews stated somewhere in the commited message after the REVIEW:
# keyword. After commited the REVIEW will be marked as submit.
# If the REVIEW: NEW sentence is found a new Review request is created and
# its id printed in the error log.
#
# INSTALLATION:
# A superuser status user is needed in order to control the reviewboard
# its name and password is stated below.
# post-review is needed in the hook dirs or in the python path and must
# be renamed to postreview.py
# It is recommended to be call from the pre-commit with
# REPOS="$1"
# TXN="$2"
# SVNLOOK=/usr/bin/svnlook
# LOG=`$SVNLOOK log -t "$TXN" "$REPOS"`
# AUTHOR=`$SVNLOOK author -t "$TXN" "$REPOS"`
# FILES=`$SVNLOOK changed -t "$TXN" "$REPOS"`
# ${REPOS}/hooks/precommit_hook.py "$REPOS" "$TXN" "$LOG" "$FILES" "$AUTHOR" || exit -1
import sys
# The reviewboar server URL for API connection
reviewboard_server="http://reviews.mitelodc.com/"
# The SVN url as reviewboard knows this repository
svn_server="https://10.112.123.143/svn/TestAutoDeploy"
# The user and password with superuser status
args=["--password=default","--username=admin"]
# The file extensions that force a review, None for all files force an
# review
#review_extensions = ['java','jsp', 'rb', 'erb', 'js', 'xls' ]
review_extensions = None
# END of configuration
repos = sys.argv[1]
txn = sys.argv[2]
log = sys.argv[3]
files = sys.argv[4]
author = sys.argv[5]
# Checks if any of the files forces a review
def need_to_be_reviewed(filesStr):
if review_extensions == None:
return True
files = filesStr.splitlines()
for fileStr in files:
(type,fileName) = fileStr.split(None,1)
extension = fileName.rsplit('.',1)
if len(extension) < 2:
continue
if (extension[1] in review_extensions):
return True
def debug(str,exit=False):
print >>sys.stderr, str
if exit:
sys.exit(-1);
if not need_to_be_reviewed(files):
sys.exit(0)
import postreview
import re
# A subclass of SVNClient that takes into account that is running in
# a precommit hook so the change hasn't been commit yet.
class SVNLookClient(postreview.SVNClient):
def __init__(self, transId, repo):
self.repo = repo
self.transId = transId
def get_repository_info(self):
return postreview.RepositoryInfo(path=svn_server,base_path='/')
def diff(self, files):
return (self.do_diff(["/usr/bin/svnlook", "diff", "-t", self.transId, self.repo] ),
None)
def svn_info(self, file):
result={}
result["URL"] = svn_server + file
result["Repository Root"] = svn_server
return result
def handle_renames(self, diff_content):
result = []
for line in diff_content:
if line.startswith('Copied:'):
copy_line = line
result.append(line)
continue
if line.startswith('---'):
if copy_line:
m = re.search(r'^Copied: .* \(from rev (\d*), (.*)\)', copy_line, re.M)
if m:
from_file = m.group(2)
rev = m.group(1)
result.append("--- %s\t2008-12-09 18:29:40 UTC (rev %s)\n" % (from_file,rev))
continue
else:
m = re.search(r'(^--- [^\t]*)\t\s*\(rev 0\)', line, re.M)
if m:
result.append(m.group(1) + "\t2008-12-09 18:29:40 UTC (rev 0)\n")
continue
if line != 67*'=' + '\n':
copy_line = None
result.append(line)
return result
client=SVNLookClient(txn,repos)
# Search for the REVIEW: line
postreview.parse_options(client,client.get_repository_info() ,args)
m = re.search(r'^REVIEW:\s*([0-9]+|new)', log, re.M | re.I )
if not m:
print >>sys.stderr, """Tag REVIEW: not found in commit log, use post-review and/or go to %s to obtain review number""" % (reviewboard_server)
exit(-1)
id=m.group(1)
p=postreview.ReviewBoardServer(reviewboard_server, client.get_repository_info(), "/tmp/cookie")
try:
p.login()
except:
print >>sys.stderr, "Wrong password, or reviewboard down."
sys.exit(-1)
if id.lower() == "new":
# Create a new request with the diff
a = p.new_review_request(None, author)
repl = re.compile(r'^REVIEW:\s*new', re.M | re.I )
log = re.sub(repl,r'REVIEW: %s' % a['id'], log)
logLines = log.splitlines()
p.set_review_request_field(a, 'summary', logLines[0])
p.set_review_request_field(a, 'description', "\n".join(logLines[1:]))
p.save_draft(a)
(diff,parent) = client.diff([])
p.upload_diff(a, diff, parent)
print >>sys.stderr, "NEW REQUEST %s\nRember to add REVIEW:%s to your comment log" % (a['id'],a['id'])
sys.exit(-1)
try:
a=p.api_get('/api/json/reviewrequests/%s/' % id)
except:
print >>sys.stderr, "Review %s not found, check the number or look here if it is right ( %sr/%s )" % (id, reviewboard_server, id)
sys.exit(-1)
if a['review_request']['status']=='submitted':
print >>sys.stderr, "The review is already submitted check the url ( %sr/%s )" % (reviewboard_server, id)
sys.exit(-1)
a=p.api_get('/api/json/reviewrequests/%s/reviews/' % id)
approved=False
for rev in a["reviews"]:
if rev['ship_it']:
approved = True
break
if not approved:
#Upload the new diff if needed
(diff,parent) = client.diff([])
p.upload_diff(p.get_review_request(id), diff, parent)
print >>sys.stderr, "The review is not approved, bother the reviewers ( %sr/%s )" % (reviewboard_server, id)
sys.exit(-1)
#TODO MOVE THIS TO OTHER STEP (like a post commit hook)
p.http_get('/api/json/reviewrequests/%s/close/submitted' % id)
#TODO Should also publish the svn revision for checking the real changes.