| [1633] | 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| [3491] | 3 | # Copyright (C) 2006-2008 Emmanuel Blot <emmanuel.blot@free.fr> |
|---|
| [1633] | 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. The terms |
|---|
| 8 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 9 | # |
|---|
| 10 | # This software consists of voluntary contributions made by many |
|---|
| 11 | # individuals. For the exact contribution history, see the revision |
|---|
| 12 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 13 | # |
|---|
| 14 | |
|---|
| 15 | import re |
|---|
| 16 | import os |
|---|
| 17 | import time |
|---|
| 18 | |
|---|
| [2109] | 19 | from genshi import Markup |
|---|
| 20 | from genshi.builder import tag |
|---|
| [1694] | 21 | from revtree.api import EmptyRangeError, RevtreeSystem |
|---|
| 22 | from revtree.model import Repository |
|---|
| [4012] | 23 | from trac.config import Option, IntOption, BoolOption, ListOption, \ |
|---|
| 24 | Section, ConfigurationError |
|---|
| [1633] | 25 | from trac.core import * |
|---|
| [1666] | 26 | from trac.perm import IPermissionRequestor |
|---|
| [1908] | 27 | from trac.util import TracError |
|---|
| [2564] | 28 | from trac.util.datefmt import format_datetime, pretty_timedelta, to_timestamp |
|---|
| [2920] | 29 | from trac.web import IRequestFilter, IRequestHandler |
|---|
| 30 | from trac.web.chrome import add_ctxtnav, add_script, add_stylesheet, \ |
|---|
| [1666] | 31 | INavigationContributor, ITemplateProvider |
|---|
| [1633] | 32 | from trac.web.href import Href |
|---|
| [1669] | 33 | from trac.wiki import wiki_to_html, WikiSystem |
|---|
| [1666] | 34 | |
|---|
| [1908] | 35 | __all__ = ['RevtreeModule'] |
|---|
| [1633] | 36 | |
|---|
| 37 | class RevtreeStore(object): |
|---|
| 38 | """User revtree properties""" |
|---|
| 39 | |
|---|
| [1908] | 40 | FIELDS = ( 'revmin', 'revmax', 'period', 'branch', 'author', |
|---|
| 41 | 'limits', 'showdel', 'style' ) |
|---|
| 42 | |
|---|
| [1652] | 43 | def __init__(self, env, authname, revspan, timebase, style): |
|---|
| [1633] | 44 | """Initialize the instance with default values""" |
|---|
| 45 | self.env = env |
|---|
| 46 | self.values = {} |
|---|
| 47 | self.revrange = None |
|---|
| 48 | self.timerange = None |
|---|
| 49 | self.revspan = revspan |
|---|
| [1908] | 50 | self.authname = (authname != 'anonymous') and authname or None |
|---|
| [1633] | 51 | self.timebase = timebase |
|---|
| 52 | self['revmin'] = str(self.revspan[0]) |
|---|
| 53 | self['revmax'] = str(self.revspan[1]) |
|---|
| [2832] | 54 | self['period'] = '31' |
|---|
| [1633] | 55 | self['limits'] = 'limperiod' |
|---|
| [1908] | 56 | self['style'] = style |
|---|
| 57 | self['branch'] = None |
|---|
| 58 | self['author'] = self.authname |
|---|
| 59 | self['showdel'] = None |
|---|
| [1633] | 60 | |
|---|
| [1908] | 61 | def __getitem__(self, name): |
|---|
| 62 | """Getter (dictionary)""" |
|---|
| 63 | return self.values[name] |
|---|
| 64 | |
|---|
| 65 | def __setitem__(self, name, value): |
|---|
| 66 | """Setter (dictionary)""" |
|---|
| 67 | self.values[name] = value |
|---|
| [1633] | 68 | |
|---|
| 69 | def load(self, session): |
|---|
| 70 | """Load user parameters from a previous session""" |
|---|
| [1908] | 71 | for field in RevtreeStore.FIELDS: |
|---|
| [1633] | 72 | key = 'revtree.%s' % field |
|---|
| 73 | if session.has_key(key): |
|---|
| 74 | self[field] = session.get(key, '') |
|---|
| 75 | |
|---|
| 76 | def save(self, session): |
|---|
| 77 | """Store user parameters""" |
|---|
| [1908] | 78 | for field in RevtreeStore.FIELDS: |
|---|
| [1633] | 79 | key = 'revtree.%s' % field |
|---|
| 80 | if self[field]: |
|---|
| [2832] | 81 | session[key] = str(self[field]) |
|---|
| [1633] | 82 | else: |
|---|
| 83 | if session.has_key(key): |
|---|
| 84 | del session[key] |
|---|
| [1908] | 85 | |
|---|
| 86 | def clear(self, session): |
|---|
| 87 | """Remove all the revtree data from the user session""" |
|---|
| 88 | for key in filter(lambda k: k.startswith('revtree'), session.keys()): |
|---|
| 89 | del session[key] |
|---|
| 90 | self.env.log.debug('Revtree data removed from user session') |
|---|
| 91 | |
|---|
| [1633] | 92 | def populate(self, values): |
|---|
| [1908] | 93 | """Populate the store from the request""" |
|---|
| 94 | for name in filter(lambda v: v in RevtreeStore.FIELDS, values.keys()): |
|---|
| [1633] | 95 | self[name] = values.get(name, '') |
|---|
| [1908] | 96 | # checkboxes need to be postprocessed |
|---|
| [2832] | 97 | self['showdel'] = values.has_key('showdel') and values['showdel'] |
|---|
| [1633] | 98 | |
|---|
| [1711] | 99 | def compute_range(self, timebase): |
|---|
| [1633] | 100 | """Computes the range of revisions to show""" |
|---|
| [1707] | 101 | self.revrange = self.revspan |
|---|
| [1633] | 102 | if self['limits'] == 'limrev': |
|---|
| 103 | self.revrange = (int(self['revmin']), int(self['revmax'])) |
|---|
| 104 | elif self['limits'] == 'limperiod': |
|---|
| 105 | period = int(self['period']) |
|---|
| 106 | if period: |
|---|
| [1711] | 107 | now = timebase |
|---|
| [1633] | 108 | self.timerange = (now-period*86400, now) |
|---|
| 109 | |
|---|
| 110 | def can_be_rendered(self): |
|---|
| 111 | """Reports whether the revtree has enough items to produce a valid |
|---|
| 112 | representation, based on the revision range""" |
|---|
| 113 | if self.timerange: |
|---|
| 114 | return True |
|---|
| 115 | if self.revrange and (self.revrange[0] < self.revrange[1]): |
|---|
| 116 | return True |
|---|
| 117 | return False |
|---|
| [1908] | 118 | |
|---|
| 119 | def get_values(self): |
|---|
| 120 | """Returns a dictionary of the stored values""" |
|---|
| 121 | return self.values |
|---|
| [1633] | 122 | |
|---|
| 123 | |
|---|
| [4012] | 124 | class FloatOption(Option): |
|---|
| 125 | """Descriptor for float configuration options. |
|---|
| 126 | Option for real number is missing in Trac |
|---|
| 127 | """ |
|---|
| 128 | |
|---|
| 129 | def accessor(self, section, name, default=''): |
|---|
| 130 | """Return the value of the specified option as float. |
|---|
| 131 | |
|---|
| 132 | If the specified option can not be converted to a float, a |
|---|
| 133 | `ConfigurationError` exception is raised. |
|---|
| 134 | |
|---|
| 135 | Valid default input is a string or a float. Returns an float. |
|---|
| 136 | """ |
|---|
| 137 | value = section.get(name, default) |
|---|
| 138 | if not value: |
|---|
| 139 | return 0.0 |
|---|
| 140 | try: |
|---|
| 141 | return float(value) |
|---|
| 142 | except ValueError: |
|---|
| 143 | raise ConfigurationError('expected real number, got %s' % \ |
|---|
| 144 | repr(value)) |
|---|
| 145 | |
|---|
| 146 | class ChoiceOption(Option): |
|---|
| 147 | """Descriptor for choice configuration options.""" |
|---|
| 148 | |
|---|
| 149 | def __init__(self, section, name, default=None, choices='', doc=''): |
|---|
| 150 | Option.__init__(self, section, name, default, doc) |
|---|
| 151 | self.choices = filter(None, [c.strip() for c in choices.split(',')]) |
|---|
| 152 | |
|---|
| 153 | def accessor(self, section, name, default): |
|---|
| 154 | value = section.get(name, default) |
|---|
| 155 | if value not in self.choices: |
|---|
| 156 | raise ConfigurationError('expected a choice among "%s", got %s' % \ |
|---|
| 157 | (', '.join(self.choices), repr(value))) |
|---|
| 158 | return value |
|---|
| 159 | |
|---|
| 160 | |
|---|
| [1633] | 161 | class RevtreeModule(Component): |
|---|
| 162 | """Implements the revision tree feature""" |
|---|
| 163 | |
|---|
| 164 | implements(IPermissionRequestor, INavigationContributor, \ |
|---|
| [2920] | 165 | IRequestFilter, IRequestHandler, ITemplateProvider) |
|---|
| [4012] | 166 | |
|---|
| 167 | # Timeline ranges |
|---|
| [1908] | 168 | PERIODS = { 1: 'day', 2: '2 days', 3: '3 days', 5: '5 days', 7:'week', |
|---|
| 169 | 14: 'fortnight', 31: 'month', 61: '2 months', |
|---|
| 170 | 91: 'quarter', 183: 'semester', 366: 'year', 0: 'all' } |
|---|
| [4012] | 171 | |
|---|
| 172 | # Configuration Options |
|---|
| 173 | branchre = Option('revtree', 'branch_re', |
|---|
| [4024] | 174 | r'^(?:(?P<branch>trunk|(?:branches|sandboxes|vendor)/' |
|---|
| 175 | r'(?P<branchname>[^/]+))|' |
|---|
| 176 | r'(?P<tag>tags/(?P<tagname>[^/]+)))(?:/(?P<path>.*))?$', |
|---|
| [4012] | 177 | doc = """Regular expression to extract branches from paths""") |
|---|
| [1633] | 178 | |
|---|
| [4012] | 179 | abstime = BoolOption('revtree', 'abstime', 'true', |
|---|
| 180 | doc = """Timeline filters start on absolute time or on the youngest |
|---|
| 181 | revision.""") |
|---|
| 182 | |
|---|
| 183 | contexts = ListOption('revtree', 'contexts', |
|---|
| 184 | doc = """Navigation contexts where the Revtree item appears. |
|---|
| 185 | If empty, the Revtree item appears in the main navigation |
|---|
| 186 | bar.""") |
|---|
| 187 | |
|---|
| 188 | trunks = ListOption('revtree', 'trunks', |
|---|
| 189 | doc = """Branches that are considered as trunks""") |
|---|
| 190 | |
|---|
| 191 | oldest = IntOption('revtree', 'revbase', '1', |
|---|
| 192 | doc = """Oldest revision to consider (older revisions are ignored)""") |
|---|
| 193 | |
|---|
| 194 | style = ChoiceOption('revtree', 'style', 'compact', 'compact,timeline', |
|---|
| 195 | doc = """Revtree style, 'compact' or 'timeline'""") |
|---|
| 196 | |
|---|
| 197 | scale = FloatOption('revtree', 'scale', '1', |
|---|
| 198 | doc = """Default rendering scale for the SVG graph""") |
|---|
| 199 | |
|---|
| [1633] | 200 | # IPermissionRequestor methods |
|---|
| 201 | |
|---|
| 202 | def get_permission_actions(self): |
|---|
| 203 | return ['REVTREE_VIEW'] |
|---|
| 204 | |
|---|
| 205 | # INavigationContributor methods |
|---|
| 206 | |
|---|
| 207 | def get_active_navigation_item(self, req): |
|---|
| 208 | return 'revtree' |
|---|
| 209 | |
|---|
| 210 | def get_navigation_items(self, req): |
|---|
| 211 | if not req.perm.has_permission('REVTREE_VIEW'): |
|---|
| 212 | return |
|---|
| [2920] | 213 | if self.contexts: |
|---|
| 214 | return |
|---|
| [2109] | 215 | yield ('mainnav', 'revtree', |
|---|
| 216 | tag.a('Rev Tree', href=req.href.revtree())) |
|---|
| [1633] | 217 | |
|---|
| [2920] | 218 | # IRequestFilter methods |
|---|
| 219 | |
|---|
| 220 | def pre_process_request(self, req, handler): |
|---|
| 221 | return handler |
|---|
| 222 | |
|---|
| 223 | def post_process_request(self, req, template, data, content_type): |
|---|
| 224 | if req.perm.has_permission('REVTREE_VIEW'): |
|---|
| [4012] | 225 | url_parts = filter(None, req.path_info.split(u'/')) |
|---|
| 226 | if url_parts and (url_parts[0] in self.contexts): |
|---|
| 227 | add_ctxtnav(req, 'Revtree' % self.contexts, |
|---|
| 228 | href=req.href.revtree()) |
|---|
| [2920] | 229 | return (template, data, content_type) |
|---|
| 230 | |
|---|
| [1633] | 231 | # IRequestHandler methods |
|---|
| 232 | |
|---|
| 233 | def match_request(self, req): |
|---|
| [1911] | 234 | match = re.match(r'/revtree(_log)?(?:/([^/]+))?', req.path_info) |
|---|
| [1666] | 235 | if match: |
|---|
| 236 | if match.group(1): |
|---|
| [1911] | 237 | req.args['logrev'] = match.group(2) |
|---|
| [1666] | 238 | return True |
|---|
| [1633] | 239 | |
|---|
| 240 | def process_request(self, req): |
|---|
| 241 | req.perm.assert_permission('REVTREE_VIEW') |
|---|
| 242 | |
|---|
| [1911] | 243 | if req.args.has_key('logrev'): |
|---|
| [1666] | 244 | return self._process_log(req) |
|---|
| 245 | else: |
|---|
| 246 | return self._process_revtree(req) |
|---|
| [1694] | 247 | |
|---|
| 248 | # ITemplateProvider |
|---|
| 249 | |
|---|
| 250 | def get_htdocs_dirs(self): |
|---|
| 251 | """Return the absolute path of a directory containing additional |
|---|
| 252 | static resources (such as images, style sheets, etc). |
|---|
| 253 | """ |
|---|
| 254 | from pkg_resources import resource_filename |
|---|
| 255 | return [('revtree', resource_filename(__name__, 'htdocs'))] |
|---|
| 256 | |
|---|
| 257 | def get_templates_dirs(self): |
|---|
| 258 | """Return the absolute path of the directory containing the provided |
|---|
| [4012] | 259 | Genshi templates. |
|---|
| [1694] | 260 | """ |
|---|
| 261 | from pkg_resources import resource_filename |
|---|
| 262 | return [resource_filename(__name__, 'templates')] |
|---|
| 263 | |
|---|
| 264 | # end of interface implementation |
|---|
| [1666] | 265 | |
|---|
| [1694] | 266 | def __init__(self): |
|---|
| 267 | """Reads the configuration and run sanity checks""" |
|---|
| [4697] | 268 | self.env.log.debug('Revtree RE: %s' % self.branchre) |
|---|
| [4012] | 269 | self.bcre = re.compile(self.branchre) |
|---|
| [2147] | 270 | self.rt = RevtreeSystem(self.env) |
|---|
| [1694] | 271 | |
|---|
| [1666] | 272 | def _process_log(self, req): |
|---|
| 273 | """Handle AJAX log requests""" |
|---|
| 274 | try: |
|---|
| [1911] | 275 | rev = int(req.args['logrev']) |
|---|
| [1666] | 276 | repos = self.env.get_repository(req.authname) |
|---|
| 277 | chgset = repos.get_changeset(rev) |
|---|
| [1669] | 278 | wikimsg = wiki_to_html(chgset.message, self.env, req, None, |
|---|
| 279 | True, False) |
|---|
| [1908] | 280 | data = { |
|---|
| [1669] | 281 | 'chgset': True, |
|---|
| 282 | 'revision': rev, |
|---|
| 283 | 'time': format_datetime(chgset.date), |
|---|
| 284 | 'age': pretty_timedelta(chgset.date, None, 3600), |
|---|
| 285 | 'author': chgset.author or 'anonymous', |
|---|
| 286 | 'message': wikimsg, |
|---|
| 287 | } |
|---|
| [1908] | 288 | return 'revtree_log.html', {'log': data}, 'application/xhtml+xml' |
|---|
| [1669] | 289 | except Exception, e: |
|---|
| 290 | raise TracError, "Invalid revision log request: %s" % e |
|---|
| [1666] | 291 | |
|---|
| 292 | def _process_revtree(self, req): |
|---|
| 293 | """Handle revtree generation requests""" |
|---|
| [1711] | 294 | tracrepos = self.env.get_repository() |
|---|
| 295 | youngest = int(tracrepos.get_youngest_rev()) |
|---|
| [2841] | 296 | oldest = max(self.oldest, int(tracrepos.get_oldest_rev())) |
|---|
| [1711] | 297 | if self.abstime: |
|---|
| 298 | timebase = int(time.time()) |
|---|
| 299 | else: |
|---|
| [2564] | 300 | timebase = to_timestamp(tracrepos.get_changeset(youngest).date) |
|---|
| [1633] | 301 | revstore = RevtreeStore(self.env, req.authname, \ |
|---|
| [2841] | 302 | (oldest, youngest), |
|---|
| [1711] | 303 | timebase, self.style) |
|---|
| [1908] | 304 | if req.args.has_key('reset') and req.args['reset']: |
|---|
| 305 | revstore.clear(req.session) |
|---|
| 306 | else: |
|---|
| 307 | revstore.load(req.session) |
|---|
| 308 | if req.args: |
|---|
| 309 | revstore.populate(req.args) |
|---|
| [1711] | 310 | revstore.compute_range(timebase) |
|---|
| [1908] | 311 | data = revstore.get_values() |
|---|
| 312 | |
|---|
| [1633] | 313 | try: |
|---|
| 314 | if not revstore.can_be_rendered(): |
|---|
| [1694] | 315 | raise EmptyRangeError |
|---|
| [1633] | 316 | repos = Repository(self.env, req.authname) |
|---|
| 317 | repos.build(self.bcre, revstore.revrange, revstore.timerange) |
|---|
| [1707] | 318 | (branches, authors) = \ |
|---|
| [1633] | 319 | self._select_parameters(repos, req, revstore) |
|---|
| [2147] | 320 | svgrevtree = self.rt.get_revtree(repos, req) |
|---|
| [1908] | 321 | if revstore['branch']: |
|---|
| 322 | sbranches = [revstore['branch']] |
|---|
| 323 | sbranches.extend(filter(lambda t: t not in sbranches, |
|---|
| 324 | self.trunks)) |
|---|
| 325 | else: |
|---|
| 326 | sbranches = None |
|---|
| 327 | sauthors = revstore['author'] and [revstore['author']] or None |
|---|
| [2832] | 328 | if revstore['showdel']: |
|---|
| 329 | hidetermbranch = False |
|---|
| 330 | else: |
|---|
| 331 | hidetermbranch = True |
|---|
| [1908] | 332 | svgrevtree.create(req, |
|---|
| 333 | revisions=revstore.revrange, |
|---|
| 334 | branches=sbranches, authors=sauthors, |
|---|
| [2832] | 335 | hidetermbranch=hidetermbranch, |
|---|
| [1908] | 336 | style=revstore['style']) |
|---|
| [1633] | 337 | svgrevtree.build() |
|---|
| [1669] | 338 | svgrevtree.render(self.scale*0.6) |
|---|
| [1916] | 339 | style = req.href.chrome('revtree/css/revtree.css') |
|---|
| 340 | svgstyle = '<?xml-stylesheet href="%s" type="text/css"?>' % style |
|---|
| 341 | data.update({ |
|---|
| [2178] | 342 | 'svg': Markup(unicode(str(svgrevtree), 'utf-8')), |
|---|
| [1916] | 343 | 'svgstyle': Markup(svgstyle) |
|---|
| 344 | }) |
|---|
| [1633] | 345 | # create and order the drop-down list content, starting with the |
|---|
| 346 | # global values |
|---|
| 347 | branches = repos.branches().keys() |
|---|
| 348 | authors = repos.authors() |
|---|
| 349 | # save the user parameters only if the tree can be rendered |
|---|
| 350 | revstore.save(req.session) |
|---|
| [1694] | 351 | except EmptyRangeError: |
|---|
| [1908] | 352 | data.update({'errormsg': \ |
|---|
| 353 | "Selected filters cannot render a revision tree"}) |
|---|
| [1633] | 354 | # restore default parameters |
|---|
| 355 | repos = Repository(self.env, req.authname) |
|---|
| [2841] | 356 | repos.build(self.bcre, revrange=(oldest, youngest)) |
|---|
| [1633] | 357 | branches = repos.branches().keys() |
|---|
| 358 | authors = repos.authors() |
|---|
| 359 | |
|---|
| 360 | revrange = repos.revision_range() |
|---|
| [2841] | 361 | revisions = self._get_ui_revisions((oldest, youngest), revrange) |
|---|
| [1908] | 362 | branches.sort() |
|---|
| 363 | # prepend the trunks to the selected branches |
|---|
| 364 | for b in filter(lambda t: t not in branches, self.trunks): |
|---|
| 365 | branches.insert(0, b) |
|---|
| 366 | branches = filter(None, branches) |
|---|
| 367 | branches.insert(0, '') |
|---|
| 368 | authors.sort() |
|---|
| 369 | authors = filter(None, authors) |
|---|
| 370 | authors.insert(0, '') |
|---|
| [1633] | 371 | |
|---|
| [1908] | 372 | dauthors = [dict(name=a, label=a or 'All') for a in authors] |
|---|
| 373 | dbranches = [dict(name=b, label=b or 'All') for b in branches] |
|---|
| 374 | |
|---|
| 375 | data.update({ |
|---|
| 376 | 'title': 'Revision Tree', |
|---|
| 377 | 'periods': self._get_periods(), |
|---|
| 378 | 'revmin': str(revrange[0]), |
|---|
| 379 | 'revmax': str(revrange[1]), |
|---|
| 380 | 'revisions': revisions, |
|---|
| 381 | 'branches': dbranches, |
|---|
| 382 | 'authors': dauthors |
|---|
| 383 | }) |
|---|
| [1633] | 384 | |
|---|
| [1908] | 385 | # add javascript for AJAX tooltips |
|---|
| 386 | add_script(req, 'revtree/js/svgtip.js') |
|---|
| 387 | # add custom stylesheet |
|---|
| [1633] | 388 | add_stylesheet(req, 'revtree/css/revtree.css') |
|---|
| [1908] | 389 | return 'revtree.html', {'rt': data}, 'application/xhtml+xml' |
|---|
| [1633] | 390 | |
|---|
| 391 | def _get_periods(self): |
|---|
| 392 | """Generates a list of periods""" |
|---|
| [1908] | 393 | periods = RevtreeModule.PERIODS |
|---|
| 394 | days = periods.keys() |
|---|
| [1633] | 395 | days.sort() |
|---|
| [1908] | 396 | return [dict(name=str(d), label=periods[d]) for d in days] |
|---|
| [1633] | 397 | |
|---|
| [1707] | 398 | def _get_ui_revisions(self, revspan, revrange): |
|---|
| [1633] | 399 | """Generates the list of displayable revisions""" |
|---|
| 400 | (revmin, revmax) = revspan |
|---|
| 401 | allrevisions = range(revmin, revmax+1) |
|---|
| 402 | allrevisions.sort() |
|---|
| 403 | revs = [c for c in allrevisions] |
|---|
| 404 | revs.reverse() |
|---|
| 405 | revisions = [] |
|---|
| 406 | for rev in revs: |
|---|
| [1908] | 407 | if len(revisions) > 50: |
|---|
| 408 | if int(rev)%100 and (rev not in revrange): |
|---|
| 409 | continue |
|---|
| 410 | elif len(revisions) > 30: |
|---|
| [1707] | 411 | if int(rev)%20 and (rev not in revrange): |
|---|
| [1633] | 412 | continue |
|---|
| 413 | elif len(revisions) > 10: |
|---|
| [1707] | 414 | if int(rev)%10 and (rev not in revrange): |
|---|
| [1633] | 415 | continue |
|---|
| 416 | revisions.append(str(rev)) |
|---|
| 417 | if revisions[-1] != str(revmin): |
|---|
| 418 | revisions.append(str(revmin)) |
|---|
| 419 | return revisions |
|---|
| 420 | |
|---|
| 421 | def _select_parameters(self, repos, req, revstore): |
|---|
| [1675] | 422 | """Calculates the revisions/branches/authors to show as selectable |
|---|
| 423 | properties for the revtree generation""" |
|---|
| [1633] | 424 | revs = [c for c in repos.changesets()] |
|---|
| 425 | revs.reverse() |
|---|
| 426 | brnames = [bn for bn in repos.branches().keys() \ |
|---|
| 427 | if bn not in self.trunks] |
|---|
| 428 | brnames.sort() |
|---|
| 429 | branches = [] |
|---|
| 430 | authors = repos.authors() |
|---|
| 431 | vbranches = None |
|---|
| [1908] | 432 | brfilter = revstore['branch'] |
|---|
| 433 | authfilter = revstore['author'] |
|---|
| [1633] | 434 | for b in brnames: |
|---|
| 435 | if brfilter and brfilter != b: |
|---|
| 436 | continue |
|---|
| 437 | if authfilter and authfilter not in repos.branch(b).authors(): |
|---|
| 438 | continue |
|---|
| 439 | branches.append(b) |
|---|
| 440 | if brfilter or authfilter: |
|---|
| 441 | vbranches = self.trunks |
|---|
| 442 | vbranches.extend(branches) |
|---|
| [1707] | 443 | return (vbranches, authors) |
|---|