39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2005 Edgewall Software
|
|
4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
|
|
5 # All rights reserved.
|
|
6 #
|
|
7 # This software is licensed as described in the file COPYING, which
|
|
8 # you should have received as part of this distribution. The terms
|
|
9 # are also available at http://trac.edgewall.com/license.html.
|
|
10 #
|
|
11 # This software consists of voluntary contributions made by many
|
|
12 # individuals. For the exact contribution history, see the revision
|
|
13 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
14 #
|
|
15 # Author: Christopher Lenz <cmlenz@gmx.de>
|
|
16
|
|
17 from heapq import heappop, heappush
|
|
18
|
|
19 from trac.config import Option
|
|
20 from trac.core import *
|
|
21 from trac.perm import PermissionError
|
|
22
|
|
23
|
|
24 class IRepositoryConnector(Interface):
|
|
25 """Extension point interface for components that provide support for a
|
|
26 specific version control system."""
|
|
27
|
|
28 def get_supported_types():
|
|
29 """Return the types of version control systems that are supported by
|
|
30 this connector, and their relative priorities.
|
|
31
|
|
32 Highest number is highest priority.
|
|
33 """
|
|
34
|
|
35 def get_repository(repos_type, repos_dir, authname):
|
|
36 """Return the Repository object for the given repository type and
|
|
37 directory.
|
|
38 """
|
|
39
|
|
40
|
|
41 class RepositoryManager(Component):
|
|
42 """Component that keeps track of the supported version control systems, and
|
|
43 provides easy access to the configured implementation."""
|
|
44
|
|
45 connectors = ExtensionPoint(IRepositoryConnector)
|
|
46
|
|
47 repository_type = Option('trac', 'repository_type', 'svn',
|
|
48 """Repository connector type. (''since 0.10'')""")
|
|
49 repository_dir = Option('trac', 'repository_dir', '',
|
|
50 """Path to local repository""")
|
|
51
|
|
52 def __init__(self):
|
|
53 self._connector = None
|
|
54
|
|
55 # Public API methods
|
|
56
|
|
57 def get_repository(self, authname):
|
|
58 if not self._connector:
|
|
59 candidates = []
|
|
60 for connector in self.connectors:
|
|
61 for repos_type_, prio in connector.get_supported_types():
|
|
62 if self.repository_type != repos_type_:
|
|
63 continue
|
|
64 heappush(candidates, (-prio, connector))
|
|
65 if not candidates:
|
|
66 raise TracError, 'Unsupported version control system "%s"' \
|
|
67 % self.repository_type
|
|
68 self._connector = heappop(candidates)[1]
|
|
69 return self._connector.get_repository(self.repository_type,
|
|
70 self.repository_dir, authname)
|
|
71
|
|
72
|
|
73 class NoSuchChangeset(TracError):
|
|
74 def __init__(self, rev):
|
|
75 TracError.__init__(self, "No changeset %s in the repository" % rev)
|
|
76
|
|
77 class NoSuchNode(TracError):
|
|
78 def __init__(self, path, rev, msg=None):
|
|
79 TracError.__init__(self, "%sNo node %s at revision %s" \
|
|
80 % (msg and '%s: ' % msg or '', path, rev))
|
|
81
|
|
82 class Repository(object):
|
|
83 """
|
|
84 Base class for a repository provided by a version control system.
|
|
85 """
|
|
86
|
|
87 def __init__(self, name, authz, log):
|
|
88 self.name = name
|
|
89 self.authz = authz or Authorizer()
|
|
90 self.log = log
|
|
91
|
|
92 def close(self):
|
|
93 """
|
|
94 Close the connection to the repository.
|
|
95 """
|
|
96 raise NotImplementedError
|
|
97
|
|
98 def get_changeset(self, rev):
|
|
99 """
|
|
100 Retrieve a Changeset object that describes the changes made in
|
|
101 revision 'rev'.
|
|
102 """
|
|
103 raise NotImplementedError
|
|
104
|
|
105 def get_changesets(self, start, stop):
|
|
106 """
|
|
107 Generate Changeset belonging to the given time period (start, stop).
|
|
108 """
|
|
109 rev = self.youngest_rev
|
|
110 while rev:
|
|
111 if self.authz.has_permission_for_changeset(rev):
|
|
112 chgset = self.get_changeset(rev)
|
|
113 if chgset.date < start:
|
|
114 return
|
|
115 if chgset.date < stop:
|
|
116 yield chgset
|
|
117 rev = self.previous_rev(rev)
|
|
118
|
|
119 def has_node(self, path, rev=None):
|
|
120 """
|
|
121 Tell if there's a node at the specified (path,rev) combination.
|
|
122
|
|
123 When `rev` is `None`, the latest revision is implied.
|
|
124 """
|
|
125 try:
|
|
126 self.get_node(path, rev)
|
|
127 return True
|
|
128 except TracError:
|
|
129 return False
|
|
130
|
|
131 def get_node(self, path, rev=None):
|
|
132 """
|
|
133 Retrieve a Node (directory or file) from the repository at the
|
|
134 given path. If the rev parameter is specified, the version of the
|
|
135 node at that revision is returned, otherwise the latest version
|
|
136 of the node is returned.
|
|
137 """
|
|
138 raise NotImplementedError
|
|
139
|
|
140 def get_oldest_rev(self):
|
|
141 """
|
|
142 Return the oldest revision stored in the repository.
|
|
143 """
|
|
144 raise NotImplementedError
|
|
145 oldest_rev = property(lambda x: x.get_oldest_rev())
|
|
146
|
|
147 def get_youngest_rev(self):
|
|
148 """
|
|
149 Return the youngest revision in the repository.
|
|
150 """
|
|
151 raise NotImplementedError
|
|
152 youngest_rev = property(lambda x: x.get_youngest_rev())
|
|
153
|
|
154 def previous_rev(self, rev):
|
|
155 """
|
|
156 Return the revision immediately preceding the specified revision.
|
|
157 """
|
|
158 raise NotImplementedError
|
|
159
|
|
160 def next_rev(self, rev, path=''):
|
|
161 """
|
|
162 Return the revision immediately following the specified revision.
|
|
163 """
|
|
164 raise NotImplementedError
|
|
165
|
|
166 def rev_older_than(self, rev1, rev2):
|
|
167 """
|
|
168 Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
|
|
169 in the revision sequence.
|
|
170 """
|
|
171 raise NotImplementedError
|
|
172
|
|
173 def get_youngest_rev_in_cache(self, db):
|
|
174 """
|
|
175 Return the youngest revision currently cached.
|
|
176 The way revisions are sequenced is version control specific.
|
|
177 By default, one assumes that the revisions are sequenced in time.
|
|
178 """
|
|
179 cursor = db.cursor()
|
|
180 cursor.execute("SELECT rev FROM revision ORDER BY time DESC LIMIT 1")
|
|
181 row = cursor.fetchone()
|
|
182 return row and row[0] or None
|
|
183
|
|
184 def get_path_history(self, path, rev=None, limit=None):
|
|
185 """
|
|
186 Retrieve all the revisions containing this path (no newer than 'rev').
|
|
187 The result format should be the same as the one of Node.get_history()
|
|
188 """
|
|
189 raise NotImplementedError
|
|
190
|
|
191 def normalize_path(self, path):
|
|
192 """
|
|
193 Return a canonical representation of path in the repos.
|
|
194 """
|
|
195 return NotImplementedError
|
|
196
|
|
197 def normalize_rev(self, rev):
|
|
198 """
|
|
199 Return a canonical representation of a revision in the repos.
|
|
200 'None' is a valid revision value and represents the youngest revision.
|
|
201 """
|
|
202 return NotImplementedError
|
|
203
|
|
204 def short_rev(self, rev):
|
|
205 """
|
|
206 Return a compact representation of a revision in the repos.
|
|
207 """
|
|
208 return self.normalize_rev(rev)
|
|
209
|
|
210 def get_changes(self, old_path, old_rev, new_path, new_rev,
|
|
211 ignore_ancestry=1):
|
|
212 """
|
|
213 Generator that yields change tuples (old_node, new_node, kind, change)
|
|
214 for each node change between the two arbitrary (path,rev) pairs.
|
|
215
|
|
216 The old_node is assumed to be None when the change is an ADD,
|
|
217 the new_node is assumed to be None when the change is a DELETE.
|
|
218 """
|
|
219 raise NotImplementedError
|
|
220
|
|
221
|
|
222 class Node(object):
|
|
223 """
|
|
224 Represents a directory or file in the repository.
|
|
225 """
|
|
226
|
|
227 DIRECTORY = "dir"
|
|
228 FILE = "file"
|
|
229
|
|
230 def __init__(self, path, rev, kind):
|
|
231 assert kind in (Node.DIRECTORY, Node.FILE), "Unknown node kind %s" % kind
|
|
232 self.path = unicode(path)
|
|
233 self.rev = rev
|
|
234 self.kind = kind
|
|
235
|
|
236 def get_content(self):
|
|
237 """
|
|
238 Return a stream for reading the content of the node. This method
|
|
239 will return None for directories. The returned object should provide
|
|
240 a read([len]) function.
|
|
241 """
|
|
242 raise NotImplementedError
|
|
243
|
|
244 def get_entries(self):
|
|
245 """
|
|
246 Generator that yields the immediate child entries of a directory, in no
|
|
247 particular order. If the node is a file, this method returns None.
|
|
248 """
|
|
249 raise NotImplementedError
|
|
250
|
|
251 def get_history(self, limit=None):
|
|
252 """
|
|
253 Generator that yields (path, rev, chg) tuples, one for each revision in which
|
|
254 the node was changed. This generator will follow copies and moves of a
|
|
255 node (if the underlying version control system supports that), which
|
|
256 will be indicated by the first element of the tuple (i.e. the path)
|
|
257 changing.
|
|
258 Starts with an entry for the current revision.
|
|
259 """
|
|
260 raise NotImplementedError
|
|
261
|
|
262 def get_previous(self):
|
|
263 """
|
|
264 Return the (path, rev, chg) tuple corresponding to the previous
|
|
265 revision for that node.
|
|
266 """
|
|
267 skip = True
|
|
268 for p in self.get_history(2):
|
|
269 if skip:
|
|
270 skip = False
|
|
271 else:
|
|
272 return p
|
|
273
|
|
274 def get_properties(self):
|
|
275 """
|
|
276 Returns a dictionary containing the properties (meta-data) of the node.
|
|
277 The set of properties depends on the version control system.
|
|
278 """
|
|
279 raise NotImplementedError
|
|
280
|
|
281 def get_content_length(self):
|
|
282 raise NotImplementedError
|
|
283 content_length = property(lambda x: x.get_content_length())
|
|
284
|
|
285 def get_content_type(self):
|
|
286 raise NotImplementedError
|
|
287 content_type = property(lambda x: x.get_content_type())
|
|
288
|
|
289 def get_name(self):
|
|
290 return self.path.split('/')[-1]
|
|
291 name = property(lambda x: x.get_name())
|
|
292
|
|
293 def get_last_modified(self):
|
|
294 raise NotImplementedError
|
|
295 last_modified = property(lambda x: x.get_last_modified())
|
|
296
|
|
297 isdir = property(lambda x: x.kind == Node.DIRECTORY)
|
|
298 isfile = property(lambda x: x.kind == Node.FILE)
|
|
299
|
|
300
|
|
301 class Changeset(object):
|
|
302 """
|
|
303 Represents a set of changes of a repository.
|
|
304 """
|
|
305
|
|
306 ADD = 'add'
|
|
307 COPY = 'copy'
|
|
308 DELETE = 'delete'
|
|
309 EDIT = 'edit'
|
|
310 MOVE = 'move'
|
|
311
|
|
312 def __init__(self, rev, message, author, date):
|
|
313 self.rev = rev
|
|
314 self.message = message
|
|
315 self.author = author
|
|
316 self.date = date
|
|
317
|
|
318 def get_properties(self):
|
|
319 """Generator that provide additional metadata for this changeset.
|
|
320
|
|
321 Each additional property is a 4 element tuple:
|
|
322 * `name` is the name of the property,
|
|
323 * `text` its value
|
|
324 * `wikiflag` indicates whether the `text` should be interpreted as
|
|
325 wiki text or not
|
|
326 * `htmlclass` enables to attach special formatting to the displayed
|
|
327 property, e.g. `'author'`, `'time'`, `'message'` or `'changeset'`.
|
|
328 """
|
|
329
|
|
330 def get_changes(self):
|
|
331 """
|
|
332 Generator that produces a (path, kind, change, base_path, base_rev)
|
|
333 tuple for every change in the changeset, where change can be one of
|
|
334 Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
|
|
335 Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
|
|
336 """
|
|
337 raise NotImplementedError
|
|
338
|
|
339
|
|
340 class PermissionDenied(PermissionError):
|
|
341 """
|
|
342 Exception raised by an authorizer if the user has insufficient permissions
|
|
343 to view a specific part of the repository.
|
|
344 """
|
|
345 def __str__(self):
|
|
346 return self.action
|
|
347
|
|
348
|
|
349 class Authorizer(object):
|
|
350 """
|
|
351 Base class for authorizers that are responsible to granting or denying
|
|
352 access to view certain parts of a repository.
|
|
353 """
|
|
354
|
|
355 def assert_permission(self, path):
|
|
356 if not self.has_permission(path):
|
|
357 raise PermissionDenied, \
|
|
358 'Insufficient permissions to access %s' % path
|
|
359
|
|
360 def assert_permission_for_changeset(self, rev):
|
|
361 if not self.has_permission_for_changeset(rev):
|
|
362 raise PermissionDenied, \
|
|
363 'Insufficient permissions to access changeset %s' % rev
|
|
364
|
|
365 def has_permission(self, path):
|
|
366 return True
|
|
367
|
|
368 def has_permission_for_changeset(self, rev):
|
|
369 return True
|