Commits

mdelagra committed 9c34cce Merge

merging in latest changes from parent project, bumping to version 3.4.0

Comments (0)

Files changed (5)

+Version 3.4.0 (1/3/10)
+
+    * major merge of latest changes from parent project
+
+    changes included below
+    
+    * added support for ReviewBoard API 2.0
+
+    * fixes for internationalization of Exception messages
+    
+    * allow outgoing reviewboard repo to be specified in .hg/hgrc
+    
+    * set branch field in review request according to the changeset branch
+
+	* Added the "server" option to specify the ReviewBoard server to be used
+	
+	* handle 201 responses correctly
+
 Version 3.3.2 (11/16/10)
 
     * improved error messaging using -g when there are no outgoing changes
File contents unchanged.

mercurial_reviewboard/__init__.py

 from mercurial import cmdutil, hg, ui, mdiff, patch, util
 from mercurial.i18n import _
 
-from reviewboard import ReviewBoard, ReviewBoardError
+from reviewboard import make_rbclient, ReviewBoardError
 
-__version__ = '3.3.2'
+
+__version__ = '3.4.0'
+
 
 def postreview(ui, repo, rev='.', **opts):
     '''post a changeset to a Review Board server
 it assumes that the upstream repository specified in .hg/hgrc is the same as
 the one known to Review Board. The other two options offer more control if
 this is not the case.
+
+The --outgoing option recognizes the path entries 'reviewboard', 'default-push'
+and 'default' in this order of precedence. 'reviewboard' may be used if the
+repository accessible to Review Board is not the upstream repository.
 '''
 
     ui.status('postreview plugin, version %s\n' % __version__)
     else:
         proxy=None
     
-    server = ui.config('reviewboard', 'server')
+    server = opts.get('server')
+    if not server:
+        server = ui.config('reviewboard', 'server')
     
-    reviewboard = ReviewBoard(server, proxy)
     ui.status('reviewboard:\t%s\n' % server)
     ui.status('\n')
     username = opts.get('username') or ui.config('reviewboard', 'user')
         ui.status('password: %s\n' % '**********')
 
     try:
-        reviewboard.login(username, password)
+        return make_rbclient(server, username, password, proxy=proxy, 
+            apiver=opts.get('apiver'))
     except ReviewBoardError, msg:
-        raise util.Abort(_(msg))
-    
-    return reviewboard
+        raise util.Abort(_(str(msg)))
+
 
 def update_review(request_id, ui, fields, diff, parentdiff, opts):
     reviewboard = getreviewboard(ui, opts)
         if opts['publish']:
             reviewboard.publish(request_id)
     except ReviewBoardError, msg:
-        raise util.Abort(_(msg))
+        raise util.Abort(_(str(msg)))
     
 def new_review(ui, fields, diff, parentdiff, opts):
     reviewboard = getreviewboard(ui, opts)
         if opts['publish']:
             reviewboard.publish(request_id)
     except ReviewBoardError, msg:
-        raise util.Abort(_(msg))
+        raise util.Abort(_(str(msg)))
     
     return request_id
 
     try:
         repositories = reviewboard.repositories()
     except ReviewBoardError, msg:
-        raise util.Abort(_(msg))
+        raise util.Abort(_(str(msg)))
 
     if not repositories:
         raise util.Abort(_('no repositories configured at %s' % server))
     remotepath = expandpath(ui, opts['outgoingrepo']).lower()
     repo_id = None
     for r in repositories:
-        if r['tool'] != 'Mercurial':
+        if r.tool != 'Mercurial':
             continue
-        if r['path'].lower() == remotepath:
-            repo_id = r['id']
-            ui.status('Using repository: %s\n' % r['name'])
+        if r.path.lower() == remotepath:
+            repo_id = r.id
+            ui.status('Using repository: %s\n' % r.name)
     if repo_id == None:
         ui.status('Repositories:\n')
         repo_ids = set()
         for r in repositories:
-            if r['tool'] != 'Mercurial':
+            if r.tool != 'Mercurial':
                 continue
-            ui.status('[%s] %s\n' % (r['id'], r['name']) )
-            repo_ids.add(str(r['id']))
+            ui.status('[%s] %s\n' % (r.id, r.name) )
+            repo_ids.add(str(r.id))
         if len(repositories) > 1:
             repo_id = ui.prompt('repository id:', 0)
             if not repo_id in repo_ids:
                 raise util.Abort(_('invalid repository ID: %s') % repo_id)
         else:
-            repo_id = repositories[0]['id']
+            repo_id = str(repositories[0].id)
             ui.status('repository id: %s\n' % repo_id)
     return repo_id
 
         else:
             description = changesets_string
         fields['description'] = description 
+        
+        fields['branch'] = c.branch()
 
     for field in ('target_groups', 'target_people'):
         if opts.get(field):
     if upstream:
         return ui.expandpath(upstream)
     else:
-        return ui.expandpath('default-push', 'default')
+        return ui.expandpath(ui.expandpath('reviewboard', 'default-push'),
+                                           'default')
+
 
 def check_parent_options(opts):
     usep = bool(opts['parent'])
          _('specify repository id on reviewboard server')),
         ('m', 'master', '',
          _('use specified revision as the parent diff base')),
+        ('', 'server', '', _('ReviewBoard server URL')),
         ('e', 'existing', '', _('existing request ID to update')),
         ('u', 'update', False, _('update the fields of an existing request')),
         ('p', 'publish', None, _('publish request immediately')),
             _('comma separated list of groups needed to review the code')),
         ('', 'username', '', _('username for the ReviewBoard site')),
         ('', 'password', '', _('password for the ReviewBoard site')),
+        ('', 'apiver', '', _('ReviewBoard API version (e.g. 1.0, 2.0)')),
         ],
         _('hg postreview [OPTION]... [REVISION]')),
 }

mercurial_reviewboard/reviewboard.py

     pass
 
 class ReviewBoardError(Exception):
-    pass
+    def __init__(self, json=None):
+        self.msg = None
+        self.code = None
+        self.tags = {}
+
+        if isinstance(json, str) or isinstance(json, unicode):
+            try:
+                json = simplejson.loads(json)
+            except:
+                self.msg = json
+                return
+
+        if json:
+            if json.has_key('err'):
+                self.msg = json['err']['msg']
+                self.code = json['err']['code']
+            for key, value in json.items():
+                if isinstance(value,unicode) or isinstance(value,str) or \
+                    key == 'fields':
+                    self.tags[key] = value
+
+    def __str__(self):
+        if self.msg:
+            return ("%s (%s)" % (self.msg, self.code)) + \
+                ''.join([("\n%s: %s" % (k, v)) for k,v in self.tags.items()])
+        else:
+            return Exception.__str__(self)
+
+class Repository:
+    """
+    Represents a ReviewBoard repository
+    """
+    def __init__(self, id, name, tool, path):
+        self.id = id
+        self.name = name
+        self.tool = tool
+        self.path = path
 
 class ReviewBoardHTTPPasswordMgr(urllib2.HTTPPasswordMgr):
     """
         self.rb_user = None
         self.rb_pass = None
 
+    def set_credentials(self, username, password):
+        self.rb_user = username
+        self.rb_pass = password
+
     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:
             # handlers are global), fall back to standard password management.
             return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
 
+class ApiRequest(urllib2.Request):
+    """
+    Allows HTTP methods other than GET and POST to be used
+    """
+    def __init__(self, method, *args, **kwargs):
+        self._method = method
+        urllib2.Request.__init__(self, *args, **kwargs)
 
-class ReviewBoard:
+    def get_method(self):
+        return self._method
+
+class HttpErrorHandler(urllib2.HTTPDefaultErrorHandler):
+    """
+    Error handler that doesn't throw an exception for any code below 400.
+    This is necessary because RB returns 2xx codes other than 200 to indicate
+    success.
+    """
+    def http_error_default(self, req, fp, code, msg, hdrs):
+        if code >= 400:
+            return urllib2.HTTPDefaultErrorHandler.http_error_default(self,
+                req, fp, code, msg, hdrs)
+        else:
+            result = urllib2.HTTPError( req.get_full_url(), code, msg, hdrs, fp)
+            result.status = code
+            return result
+
+class HttpClient:
     def __init__(self, url, proxy=None):
         if not url.endswith('/'):
             url = url + '/'
             homepath = ''
         self.cookie_file = os.path.join(homepath, ".post-review-cookies.txt")
         self._cj = cookielib.MozillaCookieJar(self.cookie_file)
-        password_mgr = ReviewBoardHTTPPasswordMgr(self.url)
+        self._password_mgr = ReviewBoardHTTPPasswordMgr(self.url)
         self._opener = opener = urllib2.build_opener(
                         urllib2.ProxyHandler(proxy),
                         urllib2.UnknownHandler(),
                         urllib2.HTTPHandler(),
-                        urllib2.HTTPDefaultErrorHandler(),
+                        HttpErrorHandler(),
                         urllib2.HTTPErrorProcessor(),
                         urllib2.HTTPCookieProcessor(self._cj),
-                        urllib2.HTTPBasicAuthHandler(password_mgr),
-                        urllib2.HTTPDigestAuthHandler(password_mgr)
+                        urllib2.HTTPBasicAuthHandler(self._password_mgr),
+                        urllib2.HTTPDigestAuthHandler(self._password_mgr)
                         )
         urllib2.install_opener(self._opener)
-        self._repositories = None
-        self._requests = None
+
+    def set_credentials(self, username, password):
+        self._password_mgr.set_credentials(username, password)
+
+    def api_request(self, method, url, fields=None, files=None):
+        """
+        Performs an API call using an HTTP request at the specified path.
+        """
+        try:
+            rsp = self._http_request(method, url, fields, files)
+            if rsp:
+                return self._process_json(rsp)
+            else:
+                return None
+        except APIError, e:
+            rsp, = e.args
+            raise ReviewBoardError(rsp)
 
     def has_valid_cookie(self):
         """
 
         return False
 
-    def login(self, username=None, password=None):
-        if not username and not password and self.has_valid_cookie():
-            return
-
-        if not username:
-            username = mercurial.ui.ui().prompt('Username: ')
-        if not password:
-            password = getpass.getpass('Password: ')
-
-        self._api_post('/api/json/accounts/login/', {
-            'username': username,
-            'password': password,
-        })
-
-    def repositories(self):
-        if not self._repositories:
-            rsp = self._api_post('/api/json/repositories/')
-            self._repositories = rsp['repositories']
-        return self._repositories
-
-    def requests(self):
-        if not self._requests:
-            rsp = self._api_post('/api/json/reviewrequests/all/')
-            self._requests = rsp['review_requests']
-        return self._requests
-
-    def users(self):
-        rsp = self._api_post('/api/json/users/')
-        self.users = rsp['users']
-        return self.users
-
-    def new_request(self, repo_id, fields={}, diff='', parentdiff=''):
-        repository_path = None
-        for r in self.repositories():
-            if r['id'] == int(repo_id):
-                repository_path = r['path']
-                break
-        if not repository_path:
-            raise ReviewBoardError, ("can't find repository with id: %s" % \
-                                        repo_id)
-
-        id = self._create_request(repository_path)
-
-        self._set_request_details(id, fields, diff, parentdiff)
-
-        return id
-
-    def update_request(self, id, fields={}, diff='', parentdiff=''):
-        request_id = None
-        for r in self.requests():
-            if r['id'] == int(id):
-                request_id = int(id)
-                break
-        if not request_id:
-            raise ReviewBoardError, ("can't find request with id: %s" % id)
-
-        self._set_request_details(request_id, fields, diff, parentdiff)
-
-        return request_id
-
-    def publish(self, id):
-        self._api_post('api/json/reviewrequests/%s/publish/' % id)
-
-    def _api_post(self, url, fields=None, files=None):
+    def _http_request(self, method, path, fields, files):
         """
-        Performs an API call using HTTP POST at the specified path.
-        """
-        try:
-            return self._process_json( self._http_post(url, fields, files) )
-        except APIError, e:
-            rsp, = e.args
-
-            raise ReviewBoardError, ("%s (%s)" % \
-                                    (rsp["err"]["msg"], rsp["err"]["code"]) )
-
-    def _http_post(self, path, fields, files=None):
-        """
-        Performs an HTTP POST on the specified path.
+        Performs an HTTP request on the specified path.
         """
         if path.startswith('/'):
             path = path[1:]
         url = urljoin(self.url, path)
-        content_type, body = self._encode_multipart_formdata(fields, files)
-        headers = {
-            'Content-Type': content_type,
-            'Content-Length': str(len(body))
-        }
+        body = None
+        headers = {}
+        if fields or files:
+            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)
+            r = ApiRequest(method, url, body, headers)
             data = urllib2.urlopen(r).read()
             self._cj.save(self.cookie_file)
             return data
         except urllib2.URLError, e:
-            raise ReviewBoardError, ("Unable to access %s.\n%s" % \
-                    (url, e))
+            if not hasattr(e, 'code'):
+                raise
+            if e.code >= 400:
+                raise ReviewBoardError(e.read())
+            else:
+                return ""
         except urllib2.HTTPError, e:
-            raise ReviewBoardError, ("Unable to access %s (%s).\n%s" % \
-                    (url, e.code, e.read()))
+            raise ReviewBoardError(e.read())
 
     def _process_json(self, data):
         """
 
         return content_type, content
 
+class ApiClient:
+    def __init__(self, httpclient):
+        self._httpclient = httpclient
+
+    def _api_request(self, method, url, fields=None, files=None):
+        return self._httpclient.api_request(method, url, fields, files)
+
+class Api20Client(ApiClient):
+    """
+    Implements the 2.0 version of the API
+    """
+
+    def __init__(self, httpclient):
+        ApiClient.__init__(self, httpclient)
+        self._repositories = None
+        self._requestcache = {}
+
+    def login(self, username=None, password=None):
+        self._httpclient.set_credentials(username, password)
+        return
+
+    def repositories(self):
+        if not self._repositories:
+            rsp = self._api_request('GET', '/api/repositories/?max-results=500')
+            self._repositories = [Repository(r['id'], r['name'], r['tool'],
+                                             r['path'])
+                                  for r in rsp['repositories']]
+        return self._repositories
+
+    def new_request(self, repo_id, fields={}, diff='', parentdiff=''):
+        req = self._create_request(repo_id)
+        self._set_request_details(req, fields, diff, parentdiff)
+        self._requestcache[req['id']] = req
+        return req['id']
+
+    def update_request(self, id, fields={}, diff='', parentdiff=''):
+        req = self._get_request(id)
+        self._set_request_details(req, fields, diff, parentdiff)
+        self.publish(id)
+
+    def publish(self, id):
+        req = self._get_request(id)
+        drafturl = req['links']['draft']['href']
+        self._api_request('PUT', drafturl, {'public':'1'})
+
+    def _create_request(self, repo_id):
+        data = { 'repository': repo_id }
+        result = self._api_request('POST', '/api/review-requests/', data)
+        return result['review_request']
+
+    def _get_request(self, id):
+        if self._requestcache.has_key(id):
+            return self._requestcache[id]
+        else:
+            result = self._api_request('GET', '/api/review-requests/%s/' % id)
+            self._requestcache[id] = result['review_request']
+            return result['review_request']
+
+    def _set_request_details(self, req, fields, diff, parentdiff):
+        if fields:
+            drafturl = req['links']['draft']['href']
+            self._api_request('PUT', drafturl, fields)
+        if diff:
+            diffurl = req['links']['diffs']['href']
+            data = {'path': {'filename': 'diff', 'content': diff}}
+            if parentdiff:
+                data['parent_diff_path'] = \
+                    {'filename': 'parent_diff', 'content': parentdiff}
+            self._api_request('POST', diffurl, {}, data)
+
+class Api10Client(ApiClient):
+    """
+    Implements the 1.0 version of the API
+    """
+
+    def __init__(self, httpclient):
+        ApiClient.__init__(self, httpclient)
+        self._repositories = None
+        self._requests = None
+
+    def _api_post(self, url, fields=None, files=None):
+        return self._api_request('POST', url, fields, files)
+
+    def login(self, username=None, password=None):
+        if not username and not password and self.client.has_valid_cookie():
+            return
+
+        if not username:
+            username = mercurial.ui.ui().prompt('Username: ')
+        if not password:
+            password = getpass.getpass('Password: ')
+
+        self._api_post('/api/json/accounts/login/', {
+            'username': username,
+            'password': password,
+        })
+
+    def repositories(self):
+        if not self._repositories:
+            rsp = self._api_post('/api/json/repositories/')
+            self._repositories = [Repository(r['id'], r['name'], r['tool'],
+                                             r['path'])
+                                  for r in rsp['repositories']]
+        return self._repositories
+
+    def requests(self):
+        if not self._requests:
+            rsp = self._api_post('/api/json/reviewrequests/all/')
+            self._requests = rsp['review_requests']
+        return self._requests
+
+    def new_request(self, repo_id, fields={}, diff='', parentdiff=''):
+        repository_path = None
+        for r in self.repositories():
+            if r.id == int(repo_id):
+                repository_path = r.path
+                break
+        if not repository_path:
+            raise ReviewBoardError, ("can't find repository with id: %s" % \
+                                        repo_id)
+
+        id = self._create_request(repository_path)
+
+        self._set_request_details(id, fields, diff, parentdiff)
+
+        return id
+
+    def update_request(self, id, fields={}, diff='', parentdiff=''):
+        request_id = None
+        for r in self.requests():
+            if r['id'] == int(id):
+                request_id = int(id)
+                break
+        if not request_id:
+            raise ReviewBoardError, ("can't find request with id: %s" % id)
+
+        self._set_request_details(request_id, fields, diff, parentdiff)
+
+        return request_id
+
+    def publish(self, id):
+        self._api_post('api/json/reviewrequests/%s/publish/' % id)
+
     def _create_request(self, repository_path):
         data = { 'repository_path': repository_path }
         rsp = self._api_post('/api/json/reviewrequests/new/', data)
         self._set_fields(id, fields)
         if diff:
             self._upload_diff(id, diff, parentdiff)
+
+
+def make_rbclient(url, username, password, proxy=None, apiver=''):
+    httpclient = HttpClient(url, proxy)
+
+    if not username:
+        username = mercurial.ui.ui().prompt('Username: ')
+    if not password:
+        password = getpass.getpass('Password: ')
+
+    httpclient.set_credentials(username, password)
+
+    if not apiver:
+        # Figure out whether the server supports API version 2.0
+        try:
+            httpclient.api_request('GET', '/api/info/')
+            apiver = '2.0'
+        except:
+            apiver = '1.0'
+            pass
+
+    if apiver == '2.0':
+        return Api20Client(httpclient)
+    elif apiver == '1.0':
+        cli = Api10Client(httpclient)
+        cli.login(username, password)
+        return cli
+    else:
+        raise Exception("Unsupported API version: %s" % apiver)

mercurial_reviewboard/tests/test_getreviewboard.py

 from mercurial_reviewboard import getreviewboard
 from mercurial_reviewboard.tests import get_initial_opts, mock_ui
 
-@patch('mercurial_reviewboard.ReviewBoard')
+
+@patch('mercurial_reviewboard.make_rbclient')
 def test_get_credentials_from_config(mock_reviewboard):
         
     # username and password configs are included 
         
     getreviewboard(ui, opts)
     
-    mock_reviewboard.return_value.login.assert_called_with('foo', 'bar')
+    mock_reviewboard.assert_called_with('http://example.com', 
+        'foo', 'bar', proxy=None, apiver='')