Changeset 3185

Show
Ignore:
Timestamp:
02/06/08 08:39:49 (1 year ago)
Author:
hvr
Message:
  • changed next revision/previous revision to point to next/previous changeset in a flattened history as provided by git-rev-list --all
  • implemented in-memory commit tree cache in order to speed up typical Trac repository access patterns (addresses #746)
  • allow to wrap GitRepository in a CachedRepository, and thus store meta-data in Trac's sql db (addresses #746)
  • implemented new [git] options to control caching:
    [git]
    
    cached_repository = true
    
    persistent_cache = true
    
  • various other fixes and cleanups
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • gitplugin/0.11/gitplugin/git_fs.py

    r3178 r3185  
    1919    Changeset, Node, Repository, IRepositoryConnector, NoSuchChangeset, NoSuchNode 
    2020from trac.wiki import IWikiSyntaxProvider 
     21from trac.versioncontrol.cache import CachedRepository 
    2122from trac.versioncontrol.web_ui import IPropertyRenderer 
     23from trac.config import _TRUE_VALUES as TRUE 
    2224 
    2325from genshi.builder import tag 
     
    2931pkg_resources.require('Trac>=0.11dev') 
    3032 
    31 from genshi.builder import tag 
    32  
    3333import PyGIT 
    3434 
    3535class GitConnector(Component): 
    3636        implements(IRepositoryConnector, IWikiSyntaxProvider, IPropertyRenderer) 
     37 
     38        def __init__(self): 
     39                self._version = None 
    3740 
    3841        def _format_sha_link(self, formatter, ns, sha, label, fullmatch=None): 
     
    5356 
    5457        def match_property(self, name, mode): 
    55                 if (name == 'Parents' and mode == 'revprop'): 
     58                if (name in ('Parents','Children') and mode == 'revprop'): 
    5659                        return 8 # default renderer has priority 1 
    5760                return 0 
    5861 
    5962        def render_property(self, name, mode, context, props): 
    60                 assert name == 'Parents' 
     63                assert name in ('Parents','Children') 
    6164 
    6265                revs = props[name] 
     
    8790 
    8891        def get_repository(self, type, dir, authname): 
     92                """GitRepository factory method""" 
     93                if not self._version: 
     94                        self._version = PyGIT.git_version() 
     95                        self.env.systeminfo.append(('GIT', self._version)) 
     96 
    8997                options = dict(self.config.options(type)) 
    90                 return GitRepository(dir, self.log, options) 
     98 
     99                repos = GitRepository(dir, self.log, options) 
     100 
     101                cached_repository = 'cached_repository' in options and options['cached_repository'] in TRUE 
     102 
     103                if cached_repository: 
     104                        repos = CachedRepository(self.env.get_db_cnx(), repos, None, self.log) 
     105                        self.log.info("enabled CachedRepository for '%s'" % dir) 
     106                else: 
     107                        self.log.info("disabled CachedRepository for '%s'" % dir) 
     108 
     109                return repos 
    91110 
    92111class GitRepository(Repository): 
    93112        def __init__(self, path, log, options): 
     113                self.logger = log 
    94114                self.gitrepo = path 
    95                 self.git = PyGIT.Storage(path) 
     115 
     116                persistent_cache = 'persistent_cache' in options and options['persistent_cache'] in TRUE 
     117 
     118                self.git = PyGIT.StorageFactory(path, log, not persistent_cache).getInstance() 
    96119                Repository.__init__(self, "git:"+path, None, log) 
    97120 
     
    99122                self.git = None 
    100123 
     124        def clear(self, youngest_rev=None): 
     125                self.youngest = None 
     126                if youngest_rev is not None: 
     127                        self.youngest = self.normalize_rev(youngest_rev) 
     128                self.oldest = None 
     129 
    101130        def get_youngest_rev(self): 
    102                 return self.git.head() 
     131                return self.git.youngest_rev() 
     132 
     133        def get_oldest_rev(self): 
     134                return self.git.oldest_rev() 
    103135 
    104136        def normalize_path(self, path): 
     
    116148                return self.git.shortrev(self.normalize_rev(rev)) 
    117149 
    118         def get_oldest_rev(self): 
    119                 return "" 
    120  
    121150        def get_node(self, path, rev=None): 
    122151                #print "get_node", path, rev 
     
    128157                        return time.mktime(dt.timetuple()) + dt.microsecond/1e6 
    129158 
    130                 for rev in self.git.history_all(to_unix(start), to_unix(stop)): 
     159                for rev in self.git.history_timerange(to_unix(start), to_unix(stop)): 
    131160                        yield self.get_changeset(rev) 
    132161 
    133162        def get_changeset(self, rev): 
    134163                """GitChangeset factory method""" 
    135                 #print "get_changeset", rev 
    136164                return GitChangeset(self.git, rev) 
    137165 
     
    142170 
    143171                for chg in self.git.diff_tree(old_rev, new_rev, self.normalize_path(new_path)): 
    144                         #print chg 
    145172                        (mode1,mode2,obj1,obj2,action,path) = chg 
    146                         kind = Node.FILE 
     173 
    147174                        if mode2[0] == '1' or mode2[0] == '1': 
    148175                                kind = Node.DIRECTORY 
    149  
    150                         if action == 'A': 
    151                                 change = Changeset.ADD 
    152                         elif action == 'M': 
    153                                 change = Changeset.EDIT 
    154                         elif action == 'D': 
    155                                 change = Changeset.DELETE 
    156176                        else: 
    157                                 raise "OhOh" 
     177                                kind = Node.FILE 
     178 
     179                        change = GitChangeset.action_map[action] 
    158180 
    159181                        old_node = None 
     
    168190 
    169191        def next_rev(self, rev, path=''): 
    170                 #print "next_rev" 
    171                 for c in self.git.children(rev): 
    172                         return c 
    173                 return None 
     192                return self.git.hist_next_revision(rev) 
    174193 
    175194        def previous_rev(self, rev): 
    176                 #print "previous_rev" 
    177                 for p in self.git.parents(rev): 
    178                         return p 
    179                 return None 
     195                return self.git.hist_prev_revision(rev) 
    180196 
    181197        def rev_older_than(self, rev1, rev2): 
    182                 rc = self.git.rev_is_anchestor(rev1,rev2) 
    183                 #rc = rev1 in self.git.history(rev2, '', skip=1) 
     198                rc = self.git.rev_is_anchestor_of(rev1, rev2) 
    184199                return rc 
    185200 
    186         def sync(self): 
    187                 #print "GitRepository.sync" 
    188                 pass 
    189  
     201        def sync(self, rev_callback=None): 
     202                if rev_callback: 
     203                        revs = set(self.git.all_revs()) 
     204 
     205                if not self.git.sync(): 
     206                        return None # nothing expected to change 
     207 
     208                if rev_callback: 
     209                        revs = set(self.git.all_revs()) - revs 
     210                        for r in revs: 
     211                                rev_callback(r) 
    190212 
    191213class GitNode(Node): 
     
    270292 
    271293class GitChangeset(Changeset): 
     294 
     295        action_map = { 
     296                'A': Changeset.ADD, 
     297                'M': Changeset.EDIT, 
     298                'D': Changeset.DELETE  
     299                } 
     300 
    272301        def __init__(self, git, sha): 
    273302                self.git = git 
     
    279308 
    280309                committer = props['committer'][0] 
     310 
     311                assert 'children' not in props 
     312                _children = list(git.children(sha)) 
     313                if _children: 
     314                        props['children'] = _children 
     315 
    281316                (user,time,tz) = committer.rsplit(None, 2) 
    282317 
     
    288323                if 'parent' in self.props: 
    289324                        properties['Parents'] = self.props['parent'] 
     325                if 'children' in self.props: 
     326                        properties['Children'] = self.props['children'] 
    290327                if 'committer' in self.props: 
    291328                        properties['git-committer'] = "\n".join(self.props['committer']) 
     
    302339                prev = self.props.has_key('parent') and self.props['parent'][0] or None 
    303340                for chg in self.git.diff_tree(prev, self.rev): 
    304                         #print chg 
    305341                        (mode1,mode2,obj1,obj2,action,path) = chg 
    306342                        kind = Node.FILE 
     
    308344                                kind = Node.DIRECTORY 
    309345 
    310                         if action == 'A': 
    311                                 change = Changeset.ADD 
    312                         elif action == 'M': 
    313                                 change = Changeset.EDIT 
    314                         elif action == 'D': 
    315                                 change = Changeset.DELETE 
    316                         else: 
    317                                 raise "OhOh" 
     346                        change = GitChangeset.action_map[action] 
    318347 
    319348                        yield (path, kind, change, path, prev) 
  • gitplugin/0.11/gitplugin/PyGIT.py

    r3177 r3185  
    1313# GNU General Public License for more details. 
    1414 
    15 import os, re, sys, time 
     15import os, re, sys, time, weakref, threading 
     16from collections import deque 
    1617#from traceback import print_stack 
    1718 
     
    2425    pass 
    2526 
     27def git_version(): 
     28    try: 
     29        (input, output, error) = os.popen3('git --version') 
     30        [v] = output.readlines() 
     31        [a,b,c] = v.strip().split() 
     32        return c 
     33    except: 
     34        raise GitError 
     35 
     36class StorageFactory: 
     37    __dict = weakref.WeakValueDictionary() 
     38    __dict_nonweak = dict() 
     39    __dict_lock = threading.Lock() 
     40 
     41    def __init__(self, repo, log, weak=True): 
     42        self.logger = log 
     43 
     44        StorageFactory.__dict_lock.acquire() 
     45 
     46        try: 
     47            i = StorageFactory.__dict[repo] 
     48        except KeyError: 
     49            i = Storage(repo, log) 
     50            StorageFactory.__dict[repo] = i 
     51 
     52        # create or remove additional reference depending on 'weak' argument 
     53        if weak: 
     54            try: 
     55                del StorageFactory.__dict_nonweak[repo] 
     56            except KeyError: 
     57                pass 
     58        else: 
     59            StorageFactory.__dict_nonweak[repo] = i 
     60 
     61        StorageFactory.__dict_lock.release() 
     62 
     63        self.__inst = i 
     64        self.__repo = repo 
     65 
     66    def getInstance(self): 
     67        is_weak = self.__repo not in StorageFactory.__dict_nonweak 
     68        self.logger.debug("requested %sPyGIT.Storage instance %d for '%s'" 
     69                          % (("","weak ")[is_weak], id(self.__inst), self.__repo)) 
     70        return self.__inst 
     71 
    2672class Storage: 
    27     def __init__(self,repo): 
     73    def __init__(self, repo, log): 
     74        self.logger = log 
     75        self.logger.debug("PyGIT.Storage instance %d constructed" % id(self)) 
     76 
    2877        self.repo = repo 
    2978        self.commit_encoding = None 
     79 
     80        self._lock = threading.Lock() 
     81        self.last_youngest_rev = -1 
     82        self._invalidate_caches() 
     83 
     84    def __del__(self): 
     85        self.logger.debug("PyGIT.Storage instance %d destructed" % id(self)) 
     86 
     87    def _invalidate_caches(self,youngest_rev=None): 
     88        self._lock.acquire() 
     89 
     90        rc = False 
     91 
     92        if self.last_youngest_rev != youngest_rev: 
     93            self.logger.debug("invalidated caches (%s != %s)" % (self.last_youngest_rev, youngest_rev)) 
     94            rc = True 
     95            self._commit_db = None 
     96            self._oldest_rev = None 
     97            self.last_youngest_rev = None 
     98 
     99        self._lock.release() 
     100        return rc 
     101 
     102    def get_commits(self): 
     103        self._lock.acquire() 
     104        if self._commit_db is None: 
     105            self.logger.debug("triggered rebuild of commit tree db for %d" % id(self)) 
     106            new_db = {} 
     107            parent = None 
     108            youngest = None 
     109            ord_rev = 0 
     110            for revs in self._git_call_f("git-rev-list --parents --all").readlines(): 
     111                revs = revs.strip().split() 
     112 
     113                rev = revs[0] 
     114                parents = set(revs[1:]) 
     115 
     116                ord_rev += 1 
     117 
     118                if not youngest: 
     119                    youngest = rev 
     120 
     121                # new_db[rev] = (children(rev), parents(rev), ordinal_id(rev)) 
     122                if new_db.has_key(rev): 
     123                    _children,_parents,_ord_rev = new_db[rev] 
     124                    assert _children 
     125                    assert not _parents 
     126                    assert _ord_rev == 0 
     127                    new_db[rev] = (_children, parents, ord_rev) 
     128                else: 
     129                    new_db[rev] = (set(), parents, ord_rev) 
     130 
     131                # update all parents(rev)'s children 
     132                for parent in parents: 
     133                    if new_db.has_key(parent): 
     134                        new_db[parent][0].add(rev) 
     135                    else: 
     136                        new_db[parent] = (set([rev]), set(), 0) # dummy ordinal_id 
     137 
     138            self._commit_db = new_db, parent 
     139            self.last_youngest_rev = youngest 
     140            self.logger.debug("rebuilt commit tree db for %d with %d entries" % (id(self),len(new_db))) 
     141 
     142        self._lock.release() 
     143 
     144        assert self._commit_db[1] is not None 
     145        assert self._commit_db[0] is not None 
     146 
     147        return self._commit_db[0] 
     148 
     149    def sync(self): 
     150        rev = self._git_call("git-rev-list -n1 --all").strip() 
     151        return self._invalidate_caches(rev) 
     152 
     153    def oldest_rev(self): 
     154        self.get_commits() # trigger commit tree db build 
     155        return self._commit_db[1] 
     156        #return self._git_call("git-rev-list --reverse --all | head -1").strip() 
     157 
     158    def youngest_rev(self): 
     159        self.get_commits() # trigger commit tree db build 
     160        return self.last_youngest_rev 
     161 
     162    def history_relative_rev(self, sha, rel_pos): 
     163        db = self.get_commits() 
     164 
     165        if sha not in db: 
     166            raise GitErrorSha 
     167 
     168        if rel_pos == 0: 
     169            return sha 
     170 
     171        lin_rev = db[sha][2] + rel_pos 
     172 
     173        if lin_rev < 1 or lin_rev > len(db): 
     174            return None 
     175 
     176        for k,v in db.iteritems(): 
     177            if v[2] == lin_rev: 
     178                return k 
     179 
     180        # should never be reached if db is consistent 
     181        raise GitError 
     182 
     183    def hist_next_revision(self, sha): 
     184        return self.history_relative_rev(sha, -1) 
     185 
     186    def hist_prev_revision(self, sha): 
     187        return self.history_relative_rev(sha, +1) 
    30188 
    31189    def _git_call_f(self,cmd): 
     
    38196 
    39197        if _profile_git_calls: 
    40             t = time.time() - t 
     198            t = time.time() - t # doesn't work actually, as popen3 runs async 
    41199            print >>sys.stderr, "GIT: took %6.2fs for '%s'" % (t, cmd) 
    42200            pass 
     
    54212        return self.commit_encoding 
    55213 
     214 
    56215    def head(self): 
    57216        "get current HEAD commit id" 
     
    60219    def verifyrev(self,rev): 
    61220        "verify/lookup given revision object and return a sha id or None if lookup failed" 
     221 
     222        db = self.get_commits() 
     223        if db.has_key(rev): 
     224            return rev 
     225 
    62226        rc=self._git_call("git-rev-parse --verify '%s'" % rev).strip() 
    63227        if len(rc)==0: 
     
    86250 
    87251    def read_commit(self, sha): 
     252        db = self.get_commits() 
     253        if sha not in db: 
     254            self.logger.info("read_commit failed for '%s'" % sha) 
     255            raise GitErrorSha 
     256 
    88257        raw = self._git_call("git-cat-file commit "+sha) 
    89258        raw = unicode(raw, self.get_commit_encoding(), 'replace') 
     
    110279        return int(self._git_call("git-cat-file -s "+sha).strip()) 
    111280 
     281    def children(self, sha): 
     282        db = self.get_commits() 
     283 
     284        try: 
     285            return list(db[sha][0]) 
     286        except KeyError: 
     287            return [] 
     288 
     289    def children_recursive(self, sha): 
     290        db = self.get_commits() 
     291 
     292        work_list = deque() 
     293        seen = set() 
     294 
     295        seen.update(db[sha][0]) 
     296        work_list.extend(db[sha][0]) 
     297 
     298        while work_list: 
     299            p = work_list.popleft() 
     300            yield p 
     301 
     302            #_children = db[p][0] 
     303            _children = db[p][0] - seen 
     304 
     305            seen.update(_children) 
     306            work_list.extend(_children) 
     307 
     308        assert len(work_list) == 0 
     309 
    112310    def parents(self, sha): 
    113         tmp=self._git_call("git-rev-list --max-count=1 --parents "+sha) 
    114         tmp=tmp.strip() 
    115         tmp=tmp.split() 
    116         return tmp[1:] 
    117  
    118     def children(self, sha): 
    119         for revs in self._git_call_f("git-rev-list --parents HEAD").readlines(): 
    120             revs = revs.strip() 
    121             revs = revs.split() 
    122             if sha in revs[1:]: 
    123                 yield revs[0] 
     311        db = self.get_commits() 
     312 
     313        try: 
     314            return list(db[sha][1]) 
     315        except KeyError: 
     316            return [] 
    124317 
    125318    def history(self, sha, path, limit=None, skip=0): 
     
    133326            yield rev.strip() 
    134327 
    135     def history_all(self, start, stop): 
     328    def all_revs(self): 
     329        return self.get_commits().iterkeys() 
     330 
     331    def history_timerange(self, start, stop): 
    136332        for rev in self._git_call_f("git-rev-list --reverse --max-age=%d --min-age=%d --all" \ 
    137333                                        % (start,stop)).readlines(): 
    138334            yield rev.strip() 
    139335 
    140     def rev_is_anchestor(self, rev1, rev2): 
     336    def rev_is_anchestor_of(self, rev1, rev2): 
     337        """return True if rev2 is successor of rev1""" 
    141338        rev1 = rev1.strip() 
    142339        rev2 = rev2.strip() 
    143         for rev in self._git_call_f("git-rev-list %s ^%s^" % (rev2,rev1)).readlines(): 
    144             if rev1 == rev.strip(): 
    145                 return True 
    146         return False 
     340        return rev2 in self.children_recursive(rev1) 
    147341 
    148342    def last_change(self, sha, path): 
     
    172366    print g.read_commit(g.head()) 
    173367    print "--------------" 
    174     print g.parents(g.head()) 
    175  
     368    p = g.parents(g.head()) 
     369    print list(p) 
     370    print "--------------" 
     371    print list(g.children(list(p)[0])) 
     372    print list(g.children(list(p)[0])) 
    176373    print "--------------" 
    177374    print g.get_commit_encoding() 
    178375    print "--------------" 
    179376    print g.get_branches() 
     377    print "--------------" 
     378    print g.hist_prev_revision(g.oldest_rev()), g.oldest_rev(), g.hist_next_revision(g.oldest_rev()) 
     379 
     380    print "--------------" 
     381    p = g.youngest_rev() 
     382    print g.hist_prev_revision(p), p, g.hist_next_revision(p) 
     383    print "--------------" 
     384    p = g.head() 
     385    for i in range(-5,5): 
     386        print i, g.history_relative_rev(p, i) 
     387 
     388    # check for loops 
     389    def check4loops(head): 
     390        print "check4loops", head 
     391        seen = set([head]) 
     392        for sha in g.children_recursive(head): 
     393            if sha in seen: 
     394                print "dupe detected :-/", sha, len(seen) 
     395                #print seen 
     396                #break 
     397            seen.add(sha) 
     398        return seen 
     399 
     400    print len(check4loops(g.parents(g.head())[0])) 
     401 
     402    #print len(check4loops(g.oldest_rev())) 
     403 
     404    #print len(list(g.children_recursive(g.oldest_rev())))