blob: a0bbefcbafbfc4e02bdd195b2781ff0abcfa4c75 [file] [log] [blame]
# Copyright (C) 2007-2015 International Business Machines Corporation and Others. All Rights Reserved.
# Review module.
# TODO: refactor ticket manipulation items into ticketmgr.
import re
import traceback
from trac.core import Component, implements
from trac.core import ComponentManager
from trac.core import TracError
from trac.util import Markup
from trac.web import IRequestHandler
from trac.web.chrome import add_stylesheet, add_script, ITemplateProvider, add_ctxtnav
from trac.versioncontrol import Changeset
from trac.web.api import IRequestFilter
from trac.wiki import wiki_to_html, format_to_oneliner, IWikiSyntaxProvider
from trac.mimeview import Context
from genshi.builder import tag
#from trac.env import IEnvironmentSetupParticipant
from trac.perm import IPermissionRequestor
from trac.config import ListOption
from icucodetools.ticketmgr import TicketManager
from pkg_resources import resource_filename #@UnresolvedImport
class ReviewModule(Component):
implements(ITemplateProvider, IRequestFilter, IRequestHandler, IPermissionRequestor)
# path to match for review
path_match = re.compile(r'/icureview/([0-9]+)')
voteable_paths = ListOption('icucodetools', 'paths', '/ticket*',
doc='List of URL paths to show reviews on. Globs are supported.')
# search for earliest match, and how many segments to include following
# trunk
# branches/maint/maint-4-8
# tags/release-2-0
branchList = [['trunk',0],['branches',2],['tags',1]]
# IPermissionRequestor methods
def get_permission_actions(self):
return ['ICUREVIEW_VIEW']
# ITemplateProvider methods
def get_templates_dirs(self):
try:
return [resource_filename(__name__, 'templates')]
except Exception, e:
self.log.warning('Could not get template dir: %s: %s' %
(type(e), e))
return ""
def get_htdocs_dirs(self):
return [('icucodetools', resource_filename(__name__, 'htdocs'))]
# IRequestFilter methods
def pre_process_request(self, req, handler):
if 'ICUREVIEW_VIEW' not in req.perm:
return handler
if self.match_ticketpage(req):
self.render_reviewlink(req)
return handler
def post_process_request(self, req, template, data, content_type):
return (template, data, content_type)
def render_reviewlink(self, req):
"""Render the "143 commits." box that shows in the topnav."""
#add_stylesheet(req, 'icucodetools/css/icuxtn.css')
els = []
ticket_mgr = TicketManager(self.compmgr)
db = self.env.get_db_cnx()
repos = self.env.get_repository()
if not repos:
raise TracError("Could not get repository for %s" % (req.authname))
revs = ticket_mgr.tkt2revs(self.log, db, repos, req, req.args['ticket'])
if not revs:
str = 'No commits.'
li = tag.li(str)
els.append(li)
else:
str = ' %d commits.' % len(revs)
href = req.href.review(req.args['ticket'])
a = tag.a('Review' + str, href=href)
li = tag.li(a)
els.append(li)
ul = tag.ul(els, class_='review')
className = ''
title = "Reviews"
add_ctxtnav(req, tag.span(ul, id='icureview', title=title, class_=className))
def match_request(self, req):
"""Is this a review URL?"""
match = re.match('/review(?:/([^/]+))?(?:/([^/]+))?(?:/(.*)$)?', req.path_info)
if match:
req.args['ticket'] = match.group(1)
return True
def match_ticketpage(self, req):
"""Is this the ticket URL?"""
match = re.match('/ticket(?:/([^/]+))?(?:/([^/]+))?(?:/(.*)$)?', req.path_info)
if match:
req.args['ticket'] = match.group(1)
return True
def pathToBranchName(self, path):
"""convert a full path name to the 'branch' it applies to."""
#return '/'.join(path.split('/')[0:2])
windex = None
win = None
for branch in self.branchList:
if(path == branch[0]): # catch changes to just 'trunk'
idx = 0
else:
idx = path.find(branch[0]+'/')
if(idx > -1 and (windex == None or windex > idx)):
windex = idx
win = branch
if windex == None:
segments = path.split('/')
return '/'.join(segments[0:2])
else:
#print "found %s foll %s @ %d" % (win[0],win[1],windex)
segments = path[windex:].split('/')
return path[:windex] + ('/'.join(segments[0:win[1]+1])) # use specified # of following segments
def changeToRange(self, c_new, change, repos):
"""preprocess a chgset.get_changes[n] entry. Returns (srcrev,dstrev,type) + change. The specially processed srcrev and dstrev are -1 for none, and the type gets munged a bit."""
# q: (u'trunk/Locale.java', 'file', 'add', None, u'-1') from r3
# q: (u'trunk/util.c', 'file', 'edit', u'trunk/util.c', u'2') from r4
c_path = change[0] # new path
# c_itemtype = change[1] # 'file' or ?
c_type = change[2]
c_oldpath = change[3]
c_dstrev = c_new
c_srcrev = c_old = int(change[4] or -1)
if(c_type in (Changeset.COPY,Changeset.MOVE)):
c_srcrev = -1
elif(c_type in (Changeset.DELETE)):
c_dstrev = -1
elif(c_type in (Changeset.EDIT, Changeset.ADD)):
if c_path != c_oldpath and c_oldpath != None and c_path != None: # did the path change? (copy or move)
if(c_old != -1): # if we have an old rev, track it
## SHOULD call repos.get_path_history(c_path, c_new, c_old)
## and then look for 'copy' or 'move' here.
## Code below will only return the EDIT (etc) operation *before* the copy/move.
# oldchange = repos.get_changeset(c_old) # old rev
# found = None
# for oldchg in oldchange.get_changes():
# if oldchg[0] == c_path or oldchg[3] == c_oldpath:
# found = oldchg
# if found:
# # "found" is the source location (pre copy)
# # however, change[] will have the correct from/to
# #
# c_type = "["+str(c_old)+":"+str(c_new)+"]"+found[2] + "+" +c_type
# else:
# c_type = "???+" + c_type
c_type = "(copy/move)+" + c_type
else:
c_type = "(???)+" + c_type
else:
c_type = c_type +" ???"
return (c_srcrev, c_dstrev, c_type) + change + (1,) # preprocessed + (change) + (mergecount)
def describeChange(self, file, change, req, db):
"""HTMLize a changeset (the 'details' column)"""
what = change[2] or 'change'
where = 'r%d:%d' % (change[0],change[1])
if(change[2] == 'move'):
url = req.href.changeset(change[1])
where = 'r%d' % change[1]
what = change[2]
elif(change[0] == -1):
if(change[1] == -1):
url = None
what = "noop"
where = None
else:
#if change[2] == 'add+commits':
url = req.href.browser(file, rev=change[1]) # 'add'
where = 'r%d' % change[1]
what = change[2]
elif(change[1] == -1):
url = None # deleted
what = "deleted"
where = None
else:
url = req.href.changeset(old_path=change[6] or file, old=change[0], new_path=change[3] or file, new=change[1])
# multi change
if(change[8]>1):
what = u"%s\u00d7%d" % (what, change[8])
# urlize
if url:
what = Markup('<a href="%s">%s</a>' % (url,what))
if where:
# search query?
return (what, tag.a(where, href=req.href.search(q=where)))
#return (what, where)
else:
# specific url
return (what, '')
def process_request(self, req):
"""This is the 'main' of this module."""
#db = self.env.get_db_cnx()
#ticketlist = {} # dict of ticket->???
#revlist = {} # dict of revision->
repos = self.env.get_repository()
context = Context.from_request(req, False)
new_path = req.args.get('new_path')
new_rev = req.args.get('new')
old_path = req.args.get('old_path')
old_rev = req.args.get('old')
new_path = repos.normalize_path(new_path)
new_rev = repos.normalize_rev(new_rev)
old_path = repos.normalize_path(old_path)
old_rev = repos.normalize_rev(old_rev)
# if not req.perm.has_permission('TICKET_MODIFY'):
# return req.redirect(req.href.browser())
old_rev = int(old_rev)
new_rev = int(new_rev)
ticket = req.args.get('ticket')
try:
ticket = int(ticket)
except Exception:
ticket = 0
# req.hdf['review.ticket'] = ticket
# req.hdf['review.tickethtml'] = tag.a(ticket, req.href.ticket(ticket))
data = {}
data['overall_y'] = 0
data['ticket_id'] = req.args['ticket']
data['ticket_summary'] = ''
data['ticket_href'] = req.href.ticket(req.args['ticket'])
ticket_mgr = TicketManager(self.compmgr)
db = self.env.get_db_cnx()
repos = self.env.get_repository()
revs = ticket_mgr.tkt2revs(self.log, db, repos, req, req.args['ticket'])
if (not revs or len(revs)==0):
# nothing to review. shouldn't happen
return ('nothing.html', data, 'text/html')
elif(len(revs)==1):
# only one change - just do a changeset view
return req.redirect(req.href.changeset(revs[0]))
revcount = 0
branches = {} # track each branch separately.
files = {} # track all of the files which are affected
# may be 0 revs.
revisions = [] # array of munged revisions
for rev in revs:
chgset = repos.get_changeset(rev)
# q: (u'trunk/Locale.java', 'file', 'add', None, u'-1') from r3
# q: (u'trunk/util.c', 'file', 'edit', u'trunk/util.c', u'2') from r4
message = chgset.message or '--'
revcount = revcount + 1
revision = {}
revision['rev'] = tag.a(rev, req.href.changeset(rev))
revision['author'] = chgset.author
revision['num'] = rev
revision['comment'] = message #wiki_to_oneliner( message, self.env, db, shorten=False )
try:
revision['comment_wiki'] = format_to_oneliner( self.env, context, message, shorten=False )
except Exception, e:
self.env.log.warn(e)
revision['comment_wiki'] = "%s (could not format - %s)" % (message, str(e))
rbranches = revision['branches'] = []
# walk through all changes in this Changeset and apply them to the files[] array
for chg in chgset.get_changes():
path = chg[0] # new path
if path in files:
item = files[path] # known file
else:
item = []
files[path] = item; # new file
item.append(self.changeToRange(rev,chg,repos))
branch_name = self.pathToBranchName(path)
if branch_name not in rbranches:
# first time we have seen this branch
rbranches.append(branch_name)
revisions.append(revision)
data['revisions'] = revisions
if(revcount > 0):
data['revcount'] = revcount
# print "files: %d" % len(files)
# go throuhg each file and calculate its minimum range
filelist = files.keys()
filelist.sort()
# print 'bar to %d len of %s' % (len(filelist),str(filelist))
# see changeToRange() for definition of the elements here.
# (oldrev, newrev, type, (change...) )
for file in filelist:
changes = files[file]
i = 0
# print " looping from %d to %d over %d " % (i,len(changes)-1,len(changes))
while len(changes)>1 and i<(len(changes)-1):
merge = None
if changes[i][1] == changes[i+1][0]: # if this change is exactly subsequent to the previous
if changes[i][0] == -1:
if changes[i][2] == Changeset.ADD and changes[i+1][2] == Changeset.EDIT:
merge = (changes[i][0],changes[i+1][1],'add+commits') # merge, retain 'first' rev
elif changes[i][2] == '(copy/move)+edit' and changes[i+1][2] == Changeset.EDIT:
merge = (changes[i][0],changes[i+1][1],'(copy/move)+edit') # retain 'first' rev
elif changes[i][2] == Changeset.EDIT and changes[i+1][2] == Changeset.EDIT:
merge = (changes[i][0],changes[i+1][1],'edit') # retain 'first' rev
if merge:
# preserve paths
changes[i+1] = merge + (changes[i+1][3], changes[i+1][4], changes[i][5]+"+"+changes[i+1][5], changes[i][6], changes[i+1][7], changes[i][8]+1)
changes = changes[:i] + changes[i+1:] # and shift down
# print "merged: %s" % str(changes)
files[file] = changes
else:
i = i + 1
# now, write 'em out
sera = 0
#files_data = []
for file in filelist:
sera = sera+1
file_data = {}
file_data['name'] = Markup('<a href="%s">%s</a>' % (req.href.browser(file),file))
branch_name = self.pathToBranchName(file)
#print "branch is: (%s)" % (branch_name)
branches_data = branches.get(branch_name, {})
files_data = branches_data.get('files',[])
changes = files[file]
cha = 0
changes_data = []
for change in changes:
cha = cha + 1
# print "%s output %s " % (file, str(change))
changes_data.append(self.describeChange(file, change, req, db))
file_data['changes'] = changes_data
if(len(changes)>1):
whathtml = self.describeChange(file, (int(changes[0][7] or -1), int(changes[len(changes)-1][1] or -1), 'overall', changes[len(changes)-1][3], None, None, changes[0][6], None, len(changes)), req, db)
file_data['overall'] = whathtml
file_data['overall_y'] = 1
data['overall_y'] = 1
else:
file_data['overall_y'] = 0
files_data.append(file_data)
# sets
branches_data['files'] = files_data
branches_data['len'] = len(files_data)
branches_data['name'] = branch_name
branches[branch_name] = branches_data
# .. convert dict to array.
branch_list = []
branch_keys = branches.keys()
branch_keys.sort()
for branch in branch_keys:
branch_list.append(branches[branch])
data['branches'] = branch_list
data['lastbranch'] = branch
data['branchcount'] = len(branches)
content_type = "text/html"
add_stylesheet(req, 'icucodetools/css/icuxtn.css')
add_script(req, 'icucodetools/js/review.js')
return 'review.html', data, content_type