~ubuntu-branches/ubuntu/natty/trac-git/natty

« back to all changes in this revision

Viewing changes to 0.11/tracext/git/git_fs.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonny Lamb
  • Date: 2008-07-12 01:43:30 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080712014330-x5pkjq97i1pvbc2x
Tags: 0.0.20080710-1
* New upstream fork for the 0.11 Trac plugin version. (Closes: #490183)
* debian/control:
  + Removed python-all-dev Build-Dep.
  + Updated Homepage field.
  + Upped version of Trac to depend on.
  + Upped Standards-Version.
  + Added XS-Python-Version: 2.5.
* debian/copyright: Updated.
* debian/dirs: Removed.
* debian/docs: Updated to point to 0.11 plugin.
* debian/postinst: Removed.
* debian/rules:
  + Updated 0.10 references to 0.11.
  + Added files to be deleted to clean target.
* debian/README.Debian: Updated.
* debian/patches/disable-installing-docs.diff: Removed.
* debian/patches/00-fix-setup.py.diff: Added to fix up the setup.py and
  rename tracext.git to gitplugin.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
#
 
3
# Copyright (C) 2006,2008 Herbert Valerio Riedel <hvr@gnu.org>
 
4
#
 
5
# This program is free software; you can redistribute it and/or
 
6
# modify it under the terms of the GNU General Public License
 
7
# as published by the Free Software Foundation; either version 2
 
8
# of the License, or (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
 
 
15
from trac.core import *
 
16
from trac.util import TracError, shorten_line
 
17
from trac.util.datefmt import FixedOffset, to_timestamp
 
18
from trac.versioncontrol.api import \
 
19
    Changeset, Node, Repository, IRepositoryConnector, NoSuchChangeset, NoSuchNode
 
20
from trac.wiki import IWikiSyntaxProvider
 
21
from trac.versioncontrol.cache import CachedRepository
 
22
from trac.versioncontrol.web_ui import IPropertyRenderer
 
23
from trac.config import BoolOption, IntOption, PathOption, Option
 
24
 
 
25
# for some reason CachedRepository doesn't pass-through short_rev()s
 
26
class CachedRepository2(CachedRepository):
 
27
        def short_rev(self, path):
 
28
                return self.repos.short_rev(path)
 
29
 
 
30
from genshi.builder import tag
 
31
from genshi.core import Markup, escape
 
32
 
 
33
from datetime import datetime
 
34
import time, sys
 
35
 
 
36
if not sys.version_info[:2] >= (2,5):
 
37
        raise TracError("python >= 2.5 dependancy not met")
 
38
 
 
39
import PyGIT
 
40
 
 
41
def _last_iterable(iterable):
 
42
        "helper for detecting last iteration in for-loop"
 
43
        i = iter(iterable)
 
44
        v = i.next()
 
45
        for nextv in i:
 
46
                yield False, v
 
47
                v = nextv
 
48
        yield True, v
 
49
 
 
50
# helper
 
51
def _parse_user_time(s):
 
52
        """parse author/committer attribute lines and return
 
53
        (user,timestamp)"""
 
54
        (user,time,tz_str) = s.rsplit(None, 2)
 
55
        tz = FixedOffset((int(tz_str)*6)/10, tz_str)
 
56
        time = datetime.fromtimestamp(float(time), tz)
 
57
        return (user,time)
 
58
 
 
59
class GitConnector(Component):
 
60
        implements(IRepositoryConnector, IWikiSyntaxProvider, IPropertyRenderer)
 
61
 
 
62
        def __init__(self):
 
63
                self._version = None
 
64
 
 
65
                try:
 
66
                        self._version = PyGIT.Storage.git_version(git_bin=self._git_bin)
 
67
                except PyGIT.GitError, e:
 
68
                        self.log.error("GitError: "+e.message)
 
69
 
 
70
                if self._version:
 
71
                        self.log.info("detected GIT version %s" % self._version['v_str'])
 
72
                        self.env.systeminfo.append(('GIT', self._version['v_str']))
 
73
                        if not self._version['v_compatible']:
 
74
                                self.log.error("GIT version %s installed not compatible (need >= %s)" %
 
75
                                               (self._version['v_str'], self._version['v_min_str']))
 
76
 
 
77
        def _format_sha_link(self, formatter, ns, sha, label, fullmatch=None):
 
78
                try:
 
79
                        changeset = self.env.get_repository().get_changeset(sha)
 
80
                        return tag.a(label, class_="changeset",
 
81
                                     title=shorten_line(changeset.message),
 
82
                                     href=formatter.href.changeset(sha))
 
83
                except TracError, e:
 
84
                        return tag.a(label, class_="missing changeset",
 
85
                                     href=formatter.href.changeset(sha),
 
86
                                     title=unicode(e), rel="nofollow")
 
87
 
 
88
        #######################
 
89
        # IPropertyRenderer
 
90
 
 
91
        # relied upon by GitChangeset
 
92
 
 
93
        def match_property(self, name, mode):
 
94
                if name in ('Parents','Children','git-committer','git-author') \
 
95
                            and mode == 'revprop':
 
96
                        return 8 # default renderer has priority 1
 
97
                return 0
 
98
 
 
99
        def render_property(self, name, mode, context, props):
 
100
                def sha_link(sha):
 
101
                        return self._format_sha_link(context, 'sha', sha, sha)
 
102
 
 
103
                if name in ('Parents','Children'):
 
104
                        revs = props[name]
 
105
 
 
106
                        return tag([tag(sha_link(rev), ', ') for rev in revs[:-1]],
 
107
                                   sha_link(revs[-1]))
 
108
 
 
109
                if name in ('git-committer', 'git-author'):
 
110
                        user_,time_ = props[name]
 
111
                        _str = user_ + " / " + time_.strftime('%Y-%m-%dT%H:%M:%SZ%z')
 
112
                        return unicode(_str)
 
113
 
 
114
                raise TracError("internal error")
 
115
 
 
116
        #######################
 
117
        # IWikiSyntaxProvider
 
118
 
 
119
        def get_wiki_syntax(self):
 
120
                yield (r'(?:\b|!)[0-9a-fA-F]{40,40}\b',
 
121
                       lambda fmt, sha, match:
 
122
                               self._format_sha_link(fmt, 'changeset', sha, sha))
 
123
 
 
124
        def get_link_resolvers(self):
 
125
                yield ('sha', self._format_sha_link)
 
126
 
 
127
        #######################
 
128
        # IRepositoryConnector
 
129
 
 
130
        _persistent_cache = BoolOption('git', 'persistent_cache', 'false',
 
131
                                       "enable persistent caching of commit tree")
 
132
 
 
133
        _cached_repository = BoolOption('git', 'cached_repository', 'false',
 
134
                                        "wrap `GitRepository` in `CachedRepository`")
 
135
 
 
136
        _shortrev_len = IntOption('git', 'shortrev_len', 7,
 
137
                                  "length rev sha sums should be tried to be abbreviated to"
 
138
                                  " (must be >= 4 and <= 40)")
 
139
 
 
140
        _git_bin = PathOption('git', 'git_bin', 'git', "file name of git executable")
 
141
 
 
142
 
 
143
        def get_supported_types(self):
 
144
                yield ("git", 8)
 
145
 
 
146
        def get_repository(self, type, dir, authname):
 
147
                """GitRepository factory method"""
 
148
                assert type == "git"
 
149
 
 
150
                if not self._version:
 
151
                        raise TracError("GIT backend not available")
 
152
                elif not self._version['v_compatible']:
 
153
                        raise TracError("GIT version %s installed not compatible (need >= %s)" %
 
154
                                        (self._version['v_str'], self._version['v_min_str']))
 
155
 
 
156
                repos = GitRepository(dir, self.log,
 
157
                                      persistent_cache=self._persistent_cache,
 
158
                                      git_bin=self._git_bin,
 
159
                                      shortrev_len=self._shortrev_len)
 
160
 
 
161
                if self._cached_repository:
 
162
                        repos = CachedRepository2(self.env.get_db_cnx(), repos, None, self.log)
 
163
                        self.log.info("enabled CachedRepository for '%s'" % dir)
 
164
                else:
 
165
                        self.log.info("disabled CachedRepository for '%s'" % dir)
 
166
 
 
167
                return repos
 
168
 
 
169
class GitRepository(Repository):
 
170
        def __init__(self, path, log, persistent_cache=False, git_bin='git', shortrev_len=7):
 
171
                self.logger = log
 
172
                self.gitrepo = path
 
173
                self._shortrev_len = max(4, min(shortrev_len, 40))
 
174
 
 
175
                self.git = PyGIT.StorageFactory(path, log, not persistent_cache,
 
176
                                                git_bin=git_bin).getInstance()
 
177
                Repository.__init__(self, "git:"+path, None, log)
 
178
 
 
179
        def close(self):
 
180
                self.git = None
 
181
 
 
182
        def clear(self, youngest_rev=None):
 
183
                self.youngest = None
 
184
                if youngest_rev is not None:
 
185
                        self.youngest = self.normalize_rev(youngest_rev)
 
186
                self.oldest = None
 
187
 
 
188
        def get_youngest_rev(self):
 
189
                return self.git.youngest_rev()
 
190
 
 
191
        def get_oldest_rev(self):
 
192
                return self.git.oldest_rev()
 
193
 
 
194
        def normalize_path(self, path):
 
195
                return path and path.strip('/') or ''
 
196
 
 
197
        def normalize_rev(self, rev):
 
198
                if not rev:
 
199
                        return self.get_youngest_rev()
 
200
                normrev=self.git.verifyrev(rev)
 
201
                if normrev is None:
 
202
                        raise NoSuchChangeset(rev)
 
203
                return normrev
 
204
 
 
205
        def short_rev(self, rev):
 
206
                return self.git.shortrev(self.normalize_rev(rev), min_len=self._shortrev_len)
 
207
 
 
208
        def get_node(self, path, rev=None):
 
209
                return GitNode(self.git, path, rev, self.log)
 
210
 
 
211
        def get_quickjump_entries(self, rev):
 
212
                for bname,bsha in self.git.get_branches():
 
213
                        yield 'branches', bname, '/', bsha
 
214
                for t in self.git.get_tags():
 
215
                        yield 'tags', t, '/', t
 
216
 
 
217
        def get_changesets(self, start, stop):
 
218
                for rev in self.git.history_timerange(to_timestamp(start), to_timestamp(stop)):
 
219
                        yield self.get_changeset(rev)
 
220
 
 
221
        def get_changeset(self, rev):
 
222
                """GitChangeset factory method"""
 
223
                return GitChangeset(self.git, rev)
 
224
 
 
225
        def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=0):
 
226
                # TODO: handle renames/copies, ignore_ancestry
 
227
                if old_path != new_path:
 
228
                        raise TracError("not supported in git_fs")
 
229
 
 
230
                for chg in self.git.diff_tree(old_rev, new_rev, self.normalize_path(new_path)):
 
231
                        (mode1,mode2,obj1,obj2,action,path,path2) = chg
 
232
 
 
233
                        kind = Node.FILE
 
234
                        if mode2.startswith('04') or mode1.startswith('04'):
 
235
                                kind = Node.DIRECTORY
 
236
 
 
237
                        change = GitChangeset.action_map[action]
 
238
 
 
239
                        old_node = None
 
240
                        new_node = None
 
241
 
 
242
                        if change != Changeset.ADD:
 
243
                                old_node = self.get_node(path, old_rev)
 
244
                        if change != Changeset.DELETE:
 
245
                                new_node = self.get_node(path, new_rev)
 
246
 
 
247
                        yield (old_node, new_node, kind, change)
 
248
 
 
249
        def next_rev(self, rev, path=''):
 
250
                return self.git.hist_next_revision(rev)
 
251
 
 
252
        def previous_rev(self, rev):
 
253
                return self.git.hist_prev_revision(rev)
 
254
 
 
255
        def rev_older_than(self, rev1, rev2):
 
256
                rc = self.git.rev_is_anchestor_of(rev1, rev2)
 
257
                return rc
 
258
 
 
259
        def clear(self, youngest_rev=None):
 
260
                self.sync()
 
261
 
 
262
        def sync(self, rev_callback=None):
 
263
                if rev_callback:
 
264
                        revs = set(self.git.all_revs())
 
265
 
 
266
                if not self.git.sync():
 
267
                        return None # nothing expected to change
 
268
 
 
269
                if rev_callback:
 
270
                        revs = set(self.git.all_revs()) - revs
 
271
                        for rev in revs:
 
272
                                rev_callback(rev)
 
273
 
 
274
class GitNode(Node):
 
275
        def __init__(self, git, path, rev, log, ls_tree_info=None):
 
276
                self.log = log
 
277
                self.git = git
 
278
                self.fs_sha = None # points to either tree or blobs
 
279
                self.fs_perm = None
 
280
                self.fs_size = None
 
281
 
 
282
                kind = Node.DIRECTORY
 
283
                p = path.strip('/')
 
284
                if p: # ie. not the root-tree
 
285
                        if not ls_tree_info:
 
286
                                ls_tree_info = git.ls_tree(rev, p) or None
 
287
                                if ls_tree_info:
 
288
                                        [ls_tree_info] = ls_tree_info
 
289
 
 
290
                        if not ls_tree_info:
 
291
                                raise NoSuchNode(path, rev)
 
292
 
 
293
                        (self.fs_perm, k, self.fs_sha, fn) = ls_tree_info
 
294
 
 
295
                        # fix-up to the last commit-rev that touched this node
 
296
                        rev = self.git.last_change(rev, p)
 
297
 
 
298
                        if k=='tree':
 
299
                                pass
 
300
                        elif k=='blob':
 
301
                                kind = Node.FILE
 
302
                        else:
 
303
                                raise TracError("internal error (got unexpected object kind '%s')" % k)
 
304
 
 
305
                self.created_path = path
 
306
                self.created_rev = rev
 
307
 
 
308
                Node.__init__(self, path, rev, kind)
 
309
 
 
310
        def __git_path(self):
 
311
                "return path as expected by PyGIT"
 
312
                p = self.path.strip('/')
 
313
                if self.isfile:
 
314
                        assert p
 
315
                        return p
 
316
                if self.isdir:
 
317
                        return p and (p + '/')
 
318
 
 
319
                raise TracError("internal error")
 
320
 
 
321
        def get_content(self):
 
322
                if not self.isfile:
 
323
                        return None
 
324
 
 
325
                return self.git.get_file(self.fs_sha)
 
326
 
 
327
        def get_properties(self):
 
328
                return self.fs_perm and {'mode': self.fs_perm } or {}
 
329
 
 
330
        def get_annotations(self):
 
331
                if not self.isfile:
 
332
                        return
 
333
 
 
334
                return [ rev for (rev,lineno) in self.git.blame(self.rev, self.__git_path()) ]
 
335
 
 
336
        def get_entries(self):
 
337
                if not self.isdir:
 
338
                        return
 
339
 
 
340
                for ent in self.git.ls_tree(self.rev, self.__git_path()):
 
341
                        yield GitNode(self.git, ent[3], self.rev, self.log, ent)
 
342
 
 
343
        def get_content_type(self):
 
344
                if self.isdir:
 
345
                        return None
 
346
 
 
347
                return ''
 
348
 
 
349
        def get_content_length(self):
 
350
                if not self.isfile:
 
351
                        return None
 
352
 
 
353
                if self.fs_size is None:
 
354
                        self.fs_size = self.git.get_obj_size(self.fs_sha)
 
355
 
 
356
                return self.fs_size
 
357
 
 
358
        def get_history(self, limit=None):
 
359
                # TODO: find a way to follow renames/copies
 
360
                for is_last,rev in _last_iterable(self.git.history(self.rev, self.__git_path(), limit)):
 
361
                        yield (self.path, rev, Changeset.EDIT if not is_last else Changeset.ADD)
 
362
 
 
363
        def get_last_modified(self):
 
364
                if not self.isfile:
 
365
                        return None
 
366
 
 
367
                try:
 
368
                        msg, props = self.git.read_commit(self.rev)
 
369
                        user,ts = _parse_user_time(props['committer'][0])
 
370
                except:
 
371
                        self.log.error("internal error (could not get timestamp from commit '%s')" % self.rev)
 
372
                        return None
 
373
 
 
374
                return ts
 
375
 
 
376
class GitChangeset(Changeset):
 
377
 
 
378
        action_map = {
 
379
                'A': Changeset.ADD,
 
380
                'M': Changeset.EDIT,
 
381
                'D': Changeset.DELETE,
 
382
                'R': Changeset.MOVE,
 
383
                'C': Changeset.COPY
 
384
                }
 
385
 
 
386
        def __init__(self, git, sha):
 
387
                self.git = git
 
388
                try:
 
389
                        (msg, props) = git.read_commit(sha)
 
390
                except PyGIT.GitErrorSha:
 
391
                        raise NoSuchChangeset(sha)
 
392
                self.props = props
 
393
 
 
394
                assert 'children' not in props
 
395
                _children = list(git.children(sha))
 
396
                if _children:
 
397
                        props['children'] = _children
 
398
 
 
399
                # use 1st committer as changeset owner/timestamp
 
400
                (user_, time_) = _parse_user_time(props['committer'][0])
 
401
 
 
402
                Changeset.__init__(self, sha, msg, user_, time_)
 
403
 
 
404
        def get_properties(self):
 
405
                properties = {}
 
406
                if 'parent' in self.props:
 
407
                        properties['Parents'] = self.props['parent']
 
408
                if 'children' in self.props:
 
409
                        properties['Children'] = self.props['children']
 
410
                if 'committer' in self.props:
 
411
                        properties['git-committer'] = \
 
412
                            _parse_user_time(self.props['committer'][0])
 
413
                if 'author' in self.props:
 
414
                        git_author = _parse_user_time(self.props['author'][0])
 
415
                        if not properties.get('git-committer') == git_author:
 
416
                                properties['git-author'] = git_author
 
417
 
 
418
                return properties
 
419
 
 
420
        def get_changes(self):
 
421
                paths_seen = set()
 
422
                for parent in self.props.get('parent', [None]):
 
423
                        for mode1,mode2,obj1,obj2,action,path1,path2 in \
 
424
                                    self.git.diff_tree(parent, self.rev, find_renames=True):
 
425
                                path = path2 or path1
 
426
                                p_path, p_rev = path1, parent
 
427
 
 
428
                                kind = Node.FILE
 
429
                                if mode2.startswith('04') or mode1.startswith('04'):
 
430
                                        kind = Node.DIRECTORY
 
431
 
 
432
                                action = GitChangeset.action_map[action[0]]
 
433
 
 
434
                                if action == Changeset.ADD:
 
435
                                        p_path = ''
 
436
                                        p_rev = None
 
437
 
 
438
                                # CachedRepository expects unique (rev, path, change_type) key
 
439
                                # this is only an issue in case of merges where files required editing
 
440
                                if path in paths_seen:
 
441
                                        continue
 
442
 
 
443
                                paths_seen.add(path)
 
444
 
 
445
                                yield (path, kind, action, p_path, p_rev)