source: revtreeplugin/0.11/revtree/model.py

Last change on this file was 4847, checked in by Emmanuel Blot, 15 years ago

Fix up Tags management:

  • Proper positioning of the tag label when a branch is tagged multiple times
  • Do not show deleted tags
  • Property svn:eol-style set to native
File size: 16.1 KB
RevLine 
[1633]1# -*- coding: utf-8 -*-
2#
[4024]3# Copyright (C) 2006-2007 Emmanuel Blot <emmanuel.blot@free.fr>x
[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
15import re
16import time
[1865]17from datetime import datetime
[1908]18
[2111]19from revtree import EmptyRangeError, BranchPathError, IRevtreeOptimizer
[1865]20from trac.core import *
21from trac.util.datefmt import utc
[1910]22from trac.util.text import to_unicode
[4024]23from trac.versioncontrol import NoSuchNode, Node as TracNode, \
24                                Changeset as TracChangeset
[1633]25
[1908]26__all__ = ['Repository']
[1633]27
[4024]28
29class Changeset(object):
[1633]30    """Represents a Subversion revision with additionnal properties"""
31
32    def __init__(self, repos, changeset):
[2111]33        # repository
[1633]34        self.repos = repos
[2111]35        # environment
36        self.env = repos.env
37        # trac changeset
[1633]38        self.changeset = changeset
[2111]39        # revision number
[1633]40        self.rev = self.changeset.rev
41        # clone information (if any)
42        self.clone = None
[2497]43        # very last changeset of a branch (deleted branch)
[1633]44        self.last = False
45        # SVN properties
46        self.properties = None
47       
[4024]48    @staticmethod
49    def get_chgset_info(tracchgset):
50        chgit = tracchgset.get_changes()
51        item = chgit.next()
52        info = {}
53        try:
54            chgit.next()
55        except StopIteration:
56            info['unique'] = True
57        else:
58            # more changes are available, i.e. this is not a simple changeset
59            info['unique'] = False
60        enum = ('path', 'kind', 'change', 'base_path', 'base_rev')
61        for (pos, name) in enumerate(enum):
62            info[name] = item[pos]
63        return info
64   
[1633]65    def __cmp__(self, other):
66        """Compares to another changeset, based on the revision number"""
67        return cmp(self.rev, other.rev)
68           
69    def _load_properties(self):
[1866]70        if not isinstance(self.properties, dict):
71            self.properties = self.repos.get_revision_properties(self.rev)
[1633]72       
73    def prop(self, prop):
[1866]74        self._load_properties()
[1910]75        uprop = to_unicode(prop)
76        return self.properties.has_key(uprop) and self.properties[uprop] or ''
[1633]77           
78    def props(self, majtype=None):
[1866]79        self._load_properties()
[1633]80        if majtype is None:
81            return self.properties
82        else:
83            props = {}
84            for (k,v) in self.properties.items():
85                items = k.split(':')
86                if len(items) and (items[0] == majtype):
87                    props[items[1]] = v
88            return props
[4024]89
90
91class BranchChangeset(Changeset):
92    """Represents a Subversion revision with lies in a regular branch"""
93   
94    def __init__(self, repos, changeset):
95        Changeset.__init__(self, repos, changeset)
96        # branch name
97        self.branchname = None
98        self.prettyname = None
99
[1633]100    def _find_simple_branch(self, bcre):
[4024]101        """A 'simple' changeset is described with a changeset whose only
102           change is a (branch) directory creation or deletion. Neither a file
103           nor a subdirectory should be altered in any way
104        """
[1633]105        change_gen = self.changeset.get_changes()
106        item = change_gen.next()
107        try:
108            change_gen.next()
109        except StopIteration:
110            pass
111        else:
112            return False
113        (path, kind, change, base_path, base_rev) = item
[4024]114        if kind is not TracNode.DIRECTORY:
[1633]115            return False
[4024]116        if change is TracChangeset.COPY:
[1633]117            path_mo = bcre.match(path)
118            src_mo = bcre.match(base_path)
[4024]119        elif change is TracChangeset.DELETE:
[1633]120            path_mo = bcre.match(base_path)
[2497]121            if path_mo and not path_mo.group('path'):
122                self.last = True
[1633]123            src_mo = False
124        else:
125            return False
126        if not path_mo:
127            return False
128        if path_mo.group('path'):
129            return False
130        if src_mo:
[2075]131            self.clone = (int(base_rev), src_mo.group('branch'))
[4024]132        self.branchname = path_mo.group('branch')
[4697]133        mo_dict = path_mo.groupdict()
134        self.prettyname = 'branchname' in mo_dict and mo_dict['branchname'] \
135                            or self.branchname
[1633]136        return True
137
138    def _find_plain_branch(self, bcre):
[4024]139        """A 'plain' changeset is a regular changeset, with file addition,
140           deletion or modification
141        """
[1633]142        branch = None
143        for item in self.changeset.get_changes():
144            (path, kind, change, base_path, base_rev) = item
145            mo = bcre.match(path)
146            if mo:
147                try:
[2109]148                    br = mo.group('branch')
[1633]149                except IndexError:
150                    raise AssertionError, "Invalid RE: missing 'branch' group"
151            else:
152                return False
153            if not branch:
154                branch = br
155            elif branch != br:
[2111]156                raise BranchPathError, "'%s' != '%s'" % (br, branch)
[1633]157        self.branchname = branch
[4697]158        mo_dict = mo.groupdict()
159        self.prettyname = 'branchname' in mo_dict and mo_dict['branchname'] \
160                            or self.branchname
[1633]161        return True
162
[4024]163    def build(self, bcre):
164        """Loads a changeset from a SVN repository
165        bcre should define two named groups 'branch' and 'path'
166        """
167        try:
168            if self._find_simple_branch(bcre):
169                return True
170            if self._find_plain_branch(bcre):
171                return True
172        except BranchPathError, e:
173            self.env.log.warn("%s @ rev %s" % (e, self.rev or 0))
174        return True
175
176
177class TagChangeset(Changeset):
178    """Represent a Subversion 'tags' which is barely not more than a regular
179       changeset tied to a specific directory
180    """
181   
182    def __init__(self, repos, changeset):
183        Changeset.__init__(self, repos, changeset)
184        self.repos = repos
185        self.name =  None
186        self.prettyname = None
187
188    def _find_tagged_changeset(self, bcre):
189        info = self.get_chgset_info(self.changeset)
190        if not info:
191            return False
192        if not info['unique']:
193            self.env.log.warn('Tag: too complex')
194            return False
195        if info['kind'] is not TracNode.DIRECTORY:
196            self.env.log.warn('Tag: not a dir: %s: %s' % \
197                                (info['kind'], info['path']))
198            return False
[4847]199        path_mo = bcre.match(info['path'])
200        if info['change'] is TracChangeset.DELETE:
201            mo_dict = path_mo.groupdict()
202            if 'tag' not in mo_dict:
203                return False
204            self.name = mo_dict['tag']
205            self.env.log.info('Tag: deleted %s' % info['path'])
206            self.last = True
207            return True 
[4024]208        if info['change'] is not TracChangeset.COPY:
209            self.env.log.warn('Tag: not a copy: %s: %s' % \
210                                (info['change'], info['path']))
211            return False
212        if not path_mo: # or not src_mo:
213            self.env.log.warn('Tag: with path: %s <- %s' % \
214                                (info['path'], info['base_path']))
215            return False
216        if path_mo.group('path'):
217            self.env.log.warn('Tag: cannot have path')
218            return False
219        try:
220            node = self.repos.get_node(info['path'], self.changeset.rev)
221        except NoSuchNode:
222            return False
223        (prev_path, prev_rev, prev_chg) = node.get_previous()
224        self.env.log.info("PREV: %s %s %s" % (prev_path, prev_rev, prev_chg))
225        self.clone = (int(prev_rev), prev_path)
[4697]226        mo_dict = path_mo.groupdict()
227        if 'tag' not in mo_dict:
228            return False
229        self.name = mo_dict['tag']
230        self.prettyname = mo_dict.setdefault('tagname', self.name)
[4024]231        return True
232
233    def build(self, bcre):
234        return self._find_tagged_changeset(bcre)
235           
236    def source(self):
237        return self.clone and self.repos.changeset(self.clone[0])
238
239
[1633]240class Branch(object):
241    """Represents a branch in Subversion, tracking the associated
242       changesets"""
243
[4024]244    def __init__(self, name, prettyname):
[1633]245        # Name (path)
246        self.name = name
[4024]247        self.prettyname = prettyname
[1633]248        # Source
249        self._source = None
250        # Changesets instances tied to the branch
251        self._changesets = []
252
253    def add_changeset(self, changeset):
254        """Adds a new changeset to the branch"""
255        self._changesets.append(changeset)
256        self._changesets.sort()
257       
258    def __len__(self):
259        """Counts the number of tracked changesets"""
260        return len(self._changesets)
261
262    def changesets(self, revrange=None):
263        """Returns the tracked changeset as a sequence"""
264        if revrange is None:
265            return self._changesets
266        else:
267            return filter(lambda c,mn=revrange[0],mx=revrange[1]: \
268                          mn <= c.rev <= mx, self._changesets)
269
270    def revision_range(self):
271        """Returns a tuple representing the extent of tracked revisions
272           (first, last)"""
273        if not self._changesets:
274            return (0, 0)
275        return (self._changesets[0].revision, self._changesets[-1].revision)
276
277    def authors(self):
278        """Returns a list of authors that have committed to the branch"""
279        authors = []
280        for chg in self._changesets:
281            author = chg.changeset.author
282            if author not in authors:
283                authors.append(author)
284        return authors
285
286    def source(self):
287        """Search for the origin of the branch"""
288        return self._source
289
290    def youngest(self):
291        if len(self._changesets) > 0:
292            return self._changesets[-1]
293        else: 
294            return None
295
296    def oldest(self):
297        if len(self._changesets) > 0:
298            return self._changesets[0]
299        else: 
300            return None
301
302    def is_active(self, range):
303        y = self.youngest()
304        if not y:
305            return False
306        if not (range[0] <= y.rev <= range[1]):
307            return False
308        if y.last:
309            return False
310        return True   
311
312    def build(self, repos):
313        if len(self._changesets) > 0:
314            clone = self._changesets[0].clone
315            if clone:
316                node = repos.find_node(clone[1], clone[0])
317                self._source = (int(node[1]), node[0])
[4024]318   
319   
[1633]320class Repository(object):
321    """Represents a Subversion repositories as a set of branches and a set
322       of changesets"""
323
324    def __init__(self, env, authname):
325        # Environment
326        self.env = env
327        # Logger
328        self.log = env.log
329        # Trac version control
330        self._crepos = self.env.get_repository(authname)
[4024]331        # Dictionary of changesets
[1633]332        self._changesets = {}
[4024]333        # Dictionary of branches
[1633]334        self._branches = {}
[4024]335        # Dictionary of tags
336        self._tags = {}
[1633]337
[4024]338    def _dispatch(self):
339        """Constructs the branch and tag dictionaries from the changeset
340           dictionary""" 
[1633]341        for chgset in self._changesets.values():
[4024]342            if isinstance(chgset, BranchChangeset):
343                br = chgset.branchname
344                if not self._branches.has_key(br):
345                    self._branches[br] = Branch(br, chgset.prettyname)
346                self._branches[br].add_changeset(chgset)
347            elif isinstance(chgset, TagChangeset):
348                if self._tags.has_key(chgset.name):
[4847]349                    if chgset.last:
350                        self.log.info('Removing deleted tag %s' % chgset.name)
351                        del self._tags[chgset.name]
352                        continue
[4024]353                    self.log.warn('Ubiquitous tag: %s', chgset.name)
354                self._tags[chgset.name] = chgset
[1633]355        map(lambda b: b.build(self), self._branches.values())
356
357    def changeset(self, revision):
358        """Returns a tracked changeset from the revision number"""
359        if self._changesets.has_key(revision):
360            return self._changesets[revision]
361        else:
362            return None
363
364    def branch(self, branchname):
[2361]365        """Returns a tracked branch from its name (path)
366           
367           branchname should be a unicode string, and should not start with
368           a leading path separator (/)
369        """
[1668]370        if not self._branches.has_key(branchname):
371            return None
372        else:
373            return self._branches[branchname]
[1633]374
375    def changesets(self):
[4024]376        """Returns the dictionary of changesets (keys are rev. numbers)"""
[1633]377        return self._changesets
378
379    def branches(self):
[4024]380        """Returns the dictionary of branches (keys are branch names)"""
[1633]381        return self._branches
[4024]382       
383    def tags(self):
384        """Returns the dictionary of tags (keys are tag names)"""
385        return self._tags
[1633]386
387    def revision_range(self):
388        """Returns a tuple representing the extent of tracked revisions
389           (first, last)"""
390        return (self._revrange)
391
392    def authors(self):
393        """Returns a list of authors that have committed to the repository"""
394        authors = []
395        for chg in self._changesets.values():
396            author = chg.changeset.author
397            if author not in authors:
398                authors.append(author)
399        return authors
400       
401    def get_revision_properties(self, revision):
[1866]402        """Returns the revision properties"""
403        changeset = self._crepos.get_changeset(revision)
[1910]404        return changeset.get_properties()
[2361]405       
406    def get_node_properties(self, path, revision):
407        return self._crepos.get_node(path, revision).get_properties()
[4024]408       
409    def get_node(self, path, revision):
410        return self._crepos.get_node(path, revision)
[1633]411                                           
412    def find_node(self, path, rev):
413        node = self._crepos.get_node(path, rev)
414        return (node.get_name(), node.rev)
415
416    def build(self, bcre, revrange=None, timerange=None):
417        """Builds an internal representation of the repository, which
418           is used to generate a graphical view of it"""
419        start = 0
420        stop = int(time.time())
421        if timerange:
422            if timerange[0]:
423                start = timerange[0]
424            if timerange[1]:
425                stop = timerange[1]
[1865]426        dtstart = datetime.fromtimestamp(start, utc)
427        dtstop = datetime.fromtimestamp(stop, utc)
428        vcchangesets = self._crepos.get_changesets(dtstart, dtstop)
[1633]429        if revrange:
430            revmin = self._crepos.get_oldest_rev()
431            revmax = self._crepos.get_youngest_rev()
432            if revrange[0]:
433                revmin = revrange[0]
434            if revrange[1]:
435                revmax = revrange[1]
436            vcsort = [(c.rev, c) for c in vcchangesets \
437                      if revmin <= c.rev <= revmax]
438        else:
439            vcsort = [(c.rev, c) for c in vcchangesets]
440        if len(vcsort) < 1:
[1694]441            raise EmptyRangeError
[1633]442        vcsort.sort()
443        self._revrange = (vcsort[0][1].rev,vcsort[-1][1].rev)
444        vcsort.reverse()
445        for (rev, vc) in vcsort:
[4024]446            info = Changeset.get_chgset_info(vc)
447            chgset = None
448            mo = info and bcre.match(info['path'])
449            if mo:
[4697]450                mo_dict = mo.groupdict()
451                if 'branch' in mo_dict and mo_dict['branch']:
[4024]452                    chgset = BranchChangeset(self, vc)
[4697]453                if 'tag' in mo_dict and mo_dict['tag']:
[4024]454                    chgset = TagChangeset(self, vc)
455            if chgset and chgset.build(bcre):
456                self._changesets[rev] = chgset
457            else:
458                self.log.warn('Changeset neither a known branch or tag: %s' % 
459                              (info or vc))
460        self._dispatch()
[1633]461
462    def __str__(self):
463        """Returns a string representation of the repository"""
464        msg = "Revision counter: %d\n" % len(self._changesets)
465        for br in self._branches.keys():
466            msg += "Branch %s, %d revisions\n" % \
467              (br, len(self._branches[br]))
468        return msg
[1694]469
Note: See TracBrowser for help on using the repository browser.