from __future__ import with_statement
'''The review data structures.
'''
import os
import messages, templates
from datetime import datetime
from mercurial import cmdutil, hg
from mercurial.node import hex
from mercurial.util import sha1
class PreexistingDatastore(Exception):
"""Raised when trying to initialize a datastore when one seems to exist."""
def __init__(self, committed):
super(PreexistingDatastore, self).__init__()
self.committed = committed
class SignoffExists(Exception):
'''Raised when trying to signoff twice without forcing.'''
pass
class CannotDeleteObject(Exception):
'''Raised when trying to delete an object that does not support deletion.'''
pass
def _parse_hgrf(repo):
"""Parse the .hgreview file and return the data inside."""
data = {}
hgrd = repo['tip']['.hgreview'].data().split('\n')
lines = [line for line in hgrd if line.strip()]
for line in lines:
label, _, path = [i.strip() for i in line.partition('=')]
if label == 'local':
data['lpath'] = path
elif label == 'remote':
data['rpath'] = path
return data
def _commitfunc(ui, repo, message, match, opts):
return repo.commit(message, opts.get('user'), opts.get('date'), match)
def _match(start):
return lambda fn: fn.startswith(start)
def _parse_data(data):
meta, _, message = data.partition('\n\n')
data = {}
for m in meta.split('\n'):
label, _, val = m.partition(':')
data[label] = val
data['message'] = message
return data
class ReviewDatastore(object):
'''The data store for all the reviews so far.'''
def __init__(self, ui, repo, lpath=None, rpath=None, create=False):
self.ui = ui
if not create:
data = _parse_hgrf(repo)
self.lpath = data['lpath']
self.rpath = data['rpath']
else:
if '.hgreview' in repo['tip']:
raise PreexistingDatastore(True)
if os.path.exists(os.path.join(repo.root, '.hgreview')):
raise PreexistingDatastore(False)
self.lpath = lpath or '.review'
self.rpath = rpath or ('../%s-review' % os.path.basename(repo.root))
root = os.path.join(repo.root, self.lpath)
self.target = repo
self.repo = hg.repository(ui, root, create)
if create:
hgrpath = os.path.join(repo.root, '.hgreview')
with open(hgrpath, 'w') as hgrf:
hgrf.write('local = %s\n' % self.lpath)
hgrf.write('remote = %s\n' % self.rpath)
repo.add(['.hgreview'])
def __getitem__(self, rev):
'''Return a ReviewChangeset for the given revision.'''
node = hex(self.target[rev].node())
return ReviewChangeset(self.ui, self.repo, node)
class ReviewChangeset(object):
'''The review data about one changeset in the target repository.'''
def __init__(self, ui, repo, node):
self.repo = repo
self.ui = ui
self.node = node
if '%s/.exists' % self.node in self.repo['tip']:
relevant = filter(_match(node), self.repo['tip'])
commentfns = filter(_match('%s/comments' % node), relevant)
signofffns = filter(_match('%s/signoffs' % node), relevant)
self.comments = [
ReviewComment(**_parse_data(self.repo['tip'][fn].data()))
for fn in commentfns
]
self.signoffs = [
ReviewSignoff(**_parse_data(self.repo['tip'][fn].data()))
for fn in signofffns
]
else:
self.comments = []
self.signoffs = []
path = os.path.join(self.repo.root, self.node)
os.mkdir(path)
with open(os.path.join(path, '.exists'), 'w') as e:
pass
cmdutil.commit(ui, self.repo, _commitfunc,
[os.path.join(path, '.exists')],
{ 'message': 'Initialize review data for changeset %s' % self.node,
'addremove': True, })
def add_signoff(self, message, opinion='', force=False):
'''Add (and commit) a signoff for the given revision.'''
existing = filter(lambda s: s.author == self.ui.username(), self.signoffs)
if existing:
if not force:
raise SignoffExists
existing[0].delete(self.ui, self.repo)
signoff = ReviewSignoff(self.ui.username(), datetime.utcnow(),
self.node, opinion, message)
signoff.commit(self.ui, self.repo)
def add_comment(self, message, filename='', lines=[]):
'''Add (and commit) a comment for the given file and lines.'''
comment = ReviewComment(self.ui.username(), datetime.utcnow(),
self.node, filename, lines, message)
comment.commit(self.ui, self.repo)
class _ReviewObject(object):
'''Some kind of object.'''
def __init__(self, container, commit_message, delete_message=None):
self.container = container
self.commit_message = commit_message
self.delete_message = delete_message
def commit(self, ui, repo):
'''Write and commit this object to the given repo.'''
path = os.path.join(repo.root, self.node, self.container)
if not os.path.exists(path):
os.mkdir(path)
data = self.render_data()
filename = sha1(data).hexdigest()
objectpath = os.path.join(path, filename)
with open(objectpath, 'w') as objectfile:
objectfile.write(data)
cmdutil.commit(ui, repo, _commitfunc, [objectpath],
{ 'message': self.commit_message % self.node, 'addremove': True, })
def delete(self, ui, repo):
'''Delete and commit this object in the given repo.'''
if not self.delete_message:
raise CannotDeleteObject
data = self.render_data()
filename = sha1(data).hexdigest()
objectpath = os.path.join(repo.root, self.node, self.container, filename)
os.remove(objectpath)
cmdutil.commit(ui, repo, _commitfunc, [objectpath],
{ 'message': self.delete_message % self.node, 'addremove': True, })
class ReviewComment(_ReviewObject):
'''A single review comment.'''
def __init__(self, author, datetime, node, filename, lines, message, **extra):
super(ReviewComment, self).__init__(
container='comments', commit_message=messages.COMMIT_COMMENT,
)
self.author = author
self.datetime = datetime
self.node = node
self.filename = filename
self.lines = lines
self.message = message
def render_data(self):
datetime = str(self.datetime)
lines = ','.join(self.lines)
return templates.COMMENT_FILE_TEMPLATE % ( self.author, datetime,
self.node, self.filename, lines, self.message )
class ReviewSignoff(_ReviewObject):
'''A single review signoff.'''
def __init__(self, author, datetime, node, opinion, message, **extra):
super(ReviewSignoff, self).__init__(
container='signoffs', commit_message=messages.COMMIT_SIGNOFF,
delete_message=messages.DELETE_SIGNOFF,
)
self.author = author
self.datetime = datetime
self.node = node
self.opinion = opinion
self.message = message
def render_data(self):
datetime = str(self.datetime)
return templates.SIGNOFF_FILE_TEMPLATE % ( self.author, datetime,
self.node, self.opinion, self.message )