Line 0
Link Here
|
|
|
1 |
# -*- coding: iso-8859-1 -*- |
2 |
# |
3 |
# Copyright (C) 2005 Edgewall Software |
4 |
# Copyright (C) 2005 Lele Gaifax <lele@metapensiero.it> |
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 |
# Author: Lele Gaifax <lele@metapensiero.it> |
15 |
|
16 |
from __future__ import generators |
17 |
|
18 |
from trac.util import TracError, NaivePopen |
19 |
from trac.versioncontrol import Changeset, Node, Repository |
20 |
from trac.versioncontrol.cache import CachedRepository, CachedChangeset |
21 |
from os import listdir, makedirs, utime, stat |
22 |
from os.path import join, isdir, split, exists |
23 |
from time import gmtime, mktime, strftime, timezone |
24 |
from shutil import copyfile |
25 |
from mimetypes import guess_type |
26 |
|
27 |
# Python 2.3+ compatibility |
28 |
try: |
29 |
reversed |
30 |
except: |
31 |
def reversed(x): |
32 |
if hasattr(x, 'keys'): |
33 |
raise ValueError("mappings do not support reverse iteration") |
34 |
i = len(x) |
35 |
while i > 0: |
36 |
i -= 1 |
37 |
yield x[i] |
38 |
|
39 |
class DarcsRepository(Repository): |
40 |
""" |
41 |
Darcs concrete implementation of a Repository. |
42 |
|
43 |
Darcs (http://www.abridgegame.org/darcs/) is a distribuited SCM, |
44 |
patch-centric instead of snapshot-centric like Subversion. The |
45 |
approach here is to assume that the history in the repository |
46 |
followed by Trac is immutable, which is not a requirement for the |
47 |
underlaying darcs. This means that on that repository should never |
48 |
be executed a `darcs unpull`, or `darcs unrecord` and the like. |
49 |
|
50 |
Given it's nature, this class tries to cache as much as possible |
51 |
the requests coming from the above Trac machinery, to avoid or |
52 |
minimize the external calls to Darcs that tend to be somewhat time |
53 |
expensive. In particular, _getNodeContent() caches any particular |
54 |
version of any file Trac asks to it, kept on the filesystem, |
55 |
inside the ``_darcs`` metadir of the controlled repository in the |
56 |
directory ``trac_cache`` (or where specified by the option |
57 |
``cachedir`` in the section ``darcs`` of the configuration. This |
58 |
may require some kind of control over the size of the cache, that |
59 |
OTOH may be completely removed whenever needed, it will be |
60 |
recreated as soon as the first request come in. |
61 |
""" |
62 |
|
63 |
def __init__(self, db, path, log, config): |
64 |
Repository.__init__(self, path, None, log) |
65 |
self.path = path |
66 |
self.db = db |
67 |
self.__youngest_rev = 0 |
68 |
self.__history = None |
69 |
self.__history_start = 0 |
70 |
self.__no_pristine = exists(join(self.path, '_darcs', 'current.none')) |
71 |
self.__darcs = config.get('darcs', 'command', 'darcs') |
72 |
if config.get('darcs', 'dont_escape_8bit'): |
73 |
self.__darcs = "DARCS_DONT_ESCAPE_8BIT=1 " + self.__darcs |
74 |
self.__cachedir = config.get('darcs', 'cachedir', |
75 |
join(self.path, '_darcs', 'trac_cache')) |
76 |
|
77 |
## Low level stuff: CamelCase is used to avoid clash with inherited |
78 |
## interface namespace. |
79 |
|
80 |
def _darcs (self, command, args=''): |
81 |
""" |
82 |
Execute a some query on the repository running an external darcs. |
83 |
|
84 |
Return the XML output of the command. |
85 |
""" |
86 |
|
87 |
command = "cd %r; TZ=UTC %s %s %s" % \ |
88 |
(self.path, self.__darcs, command, args) |
89 |
self.log.debug(command) |
90 |
np = NaivePopen (command, capturestderr=True) |
91 |
if np.errorlevel: |
92 |
err = 'Running (%s) failed: %s, %s.' % (command, |
93 |
np.errorlevel, np.err) |
94 |
raise TracError (err, title='Darcs execution failed') |
95 |
return np.out |
96 |
|
97 |
def _changes(self, args, startfrom=1): |
98 |
""" |
99 |
Get changes information parsing the XML output of ``darcs changes``. |
100 |
|
101 |
Return a sequence of Changesets. |
102 |
""" |
103 |
|
104 |
return changesets_from_darcschanges( |
105 |
self._darcs('changes --reverse --summary --xml', args), |
106 |
self, startfrom) |
107 |
|
108 |
def _diff(self, path, rev1, rev2=None, patch=None): |
109 |
""" |
110 |
Return a darcs diff between two revisions. |
111 |
""" |
112 |
|
113 |
diff = "diff --unified" |
114 |
rev1 = self.normalize_rev(rev1) |
115 |
cset1 = DarcsCachedChangeset(rev1, self.db) |
116 |
diff += " --from-match 'hash %s'" % cset1.hash |
117 |
if rev2: |
118 |
rev2 = self.normalize_rev(rev2) |
119 |
cset2 = DarcsCachedChangeset(rev2, self.db) |
120 |
diff += " --to-match 'hash %s'" % cset2.hash |
121 |
if patch: |
122 |
path = path + patch |
123 |
return self._darcs(diff, path) |
124 |
|
125 |
def _parseInventory(self, inventory): |
126 |
""" |
127 |
Parse an inventory, and return a dictionary of its content. |
128 |
""" |
129 |
|
130 |
from sha import new |
131 |
|
132 |
csmap = {} |
133 |
start = 0 |
134 |
length = len(inventory) |
135 |
index = self.__youngest_rev |
136 |
while start<length: |
137 |
start = inventory.find('[', start) |
138 |
if start<0: |
139 |
break |
140 |
end = inventory.index('\n', start) |
141 |
patchname = inventory[start+1:end] |
142 |
start = end |
143 |
end = inventory.index('*', start) |
144 |
author = inventory[start+1:end] |
145 |
inv = inventory[end+1] == '*' and 'f' or 't' |
146 |
date = inventory[end+2:end+16] |
147 |
y = int(date[:4]) |
148 |
m = int(date[4:6]) |
149 |
d = int(date[6:8]) |
150 |
hh = int(date[8:10]) |
151 |
mm = int(date[10:12]) |
152 |
ss = int(date[12:14]) |
153 |
unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone |
154 |
end += 16 |
155 |
log = '' |
156 |
if inventory[end] <> ']': |
157 |
start = end |
158 |
end = inventory.index('\n', start+1) |
159 |
while True: |
160 |
log += inventory[start+2:end] |
161 |
start = end |
162 |
if inventory[end+1] == ']': |
163 |
break |
164 |
end = inventory.index('\n', start+1) |
165 |
end += 1 |
166 |
|
167 |
index += 1 |
168 |
phash = new() |
169 |
phash.update(patchname) |
170 |
phash.update(author) |
171 |
phash.update(date) |
172 |
phash.update(log) |
173 |
phash.update(inv) |
174 |
patchid = '%s-%s-%s.gz' % (date, |
175 |
new(author).hexdigest()[:5], |
176 |
phash.hexdigest()) |
177 |
csmap[index] = patchid |
178 |
csmap[patchid] = index |
179 |
start = end+1 |
180 |
self.__youngest_rev = index |
181 |
return csmap |
182 |
|
183 |
def _loadChangesetsIndex(self): |
184 |
""" |
185 |
Load the index of changesets in the repository, assigning an unique |
186 |
integer number to each one, ala Subversion, for easier references. |
187 |
|
188 |
This is done by parsing the ``_darcs/inventory`` file using the |
189 |
position of each changeset as its `revision number`, **assuming** |
190 |
that **nobody's never** going to do anything that alters its order, |
191 |
such as ``darcs optimize`` or ``darcs unpull``. |
192 |
""" |
193 |
|
194 |
self.__youngest_rev = 0 |
195 |
inventories = [join(self.path, '_darcs', 'inventories', i) for i |
196 |
in listdir(join(self.path, '_darcs', 'inventories'))] |
197 |
inventories.sort() |
198 |
inventories.append(join(self.path, '_darcs', 'inventory')) |
199 |
for inventory in inventories: |
200 |
f = open(inventory, 'rU') |
201 |
try: |
202 |
index = self._parseInventory(f.read()) |
203 |
finally: |
204 |
f.close() |
205 |
|
206 |
## Medium level, work-horse methods |
207 |
|
208 |
def _getCachedContentLocation(self, node): |
209 |
""" |
210 |
Return the location of the cache for the given node. If it does |
211 |
not exist, compute it by applying a diff to the current version. |
212 |
This may return None, if the node does not actually exist. |
213 |
""" |
214 |
|
215 |
rev = self.normalize_rev(node.rev) |
216 |
|
217 |
if self.__no_pristine: |
218 |
current = join(self.path, node.path) |
219 |
else: |
220 |
current = join(self.path, '_darcs', 'current', node.path) |
221 |
|
222 |
# Iterate over history to find out which is the revision of |
223 |
# the given path that last changed the it. We need to find |
224 |
# both a 'last revision' and 'second last', because later |
225 |
# we may apply either a r1:last diff or a 2nd:current diff. |
226 |
history = self._getPathHistory(node.path, None) |
227 |
try: |
228 |
lastnode = history.next() |
229 |
except StopIteration: |
230 |
lastnode = None |
231 |
|
232 |
if lastnode is None: |
233 |
return None |
234 |
elif lastnode.rev <= rev: |
235 |
# Content hasn't changed, return current version |
236 |
if exists(current): |
237 |
return current |
238 |
|
239 |
prevlast = lastnode |
240 |
for oldnode in history: |
241 |
if oldnode.rev <= rev: |
242 |
lastnode = oldnode |
243 |
break |
244 |
prevlast = oldnode |
245 |
|
246 |
cachedir = join(self.__cachedir, str(lastnode.rev)) |
247 |
cache = join(cachedir, lastnode.path) |
248 |
|
249 |
# One may never know: should by any chance an absolute path survived |
250 |
# in lastnode.path, or in some clever way introduced some trick like |
251 |
# 'somepath/../../../etc/passwd'... |
252 |
from os.path import normpath |
253 |
assert normpath(cache).startswith(cachedir) |
254 |
|
255 |
if not exists(cache): |
256 |
self.log.debug('Caching revision %d of %s' % (lastnode.rev, |
257 |
lastnode.path)) |
258 |
dir = split(cache)[0] |
259 |
if not exists(dir): |
260 |
makedirs(dir) |
261 |
|
262 |
# If the file doesn't current exist, create an empty file |
263 |
# and apply a patch from revision 1 to the node revision, |
264 |
# otherwise apply a reversed patch from the current revision |
265 |
# and to node revision+1. |
266 |
try: |
267 |
if not exists(current): |
268 |
self.log.debug('Applying a direct patch from revision 1 up' |
269 |
' to %d to %s' % (lastnode.rev, node.path)) |
270 |
open(cache, 'w').close() |
271 |
patch = "| patch -p1 -d %s" % cachedir |
272 |
self._diff(node.path, 1, lastnode.rev, patch=patch) |
273 |
else: |
274 |
self.log.debug('Applying a reverse patch from current' |
275 |
' revision back to %d to %s' % |
276 |
(lastnode.rev, node.path)) |
277 |
copyfile(current, cache) |
278 |
patch = "| patch -p1 -R -d %s" % cachedir |
279 |
self._diff(node.path, prevlast.rev, patch=patch) |
280 |
except TracError, exc: |
281 |
if 'Only garbage was found in the patch input' in exc.message: |
282 |
pass |
283 |
else: |
284 |
raise |
285 |
|
286 |
# Adjust the times of the just created cache file, to match |
287 |
# the timestamp of the associated changeset. |
288 |
cursor = self.db.cursor() |
289 |
cursor.execute("SELECT time FROM revision " |
290 |
"WHERE rev = %s", (lastnode.rev,)) |
291 |
cstimestamp = int(cursor.fetchone()[0]) |
292 |
utime(cache, (cstimestamp, cstimestamp)) |
293 |
|
294 |
if exists(cache): |
295 |
return cache |
296 |
else: |
297 |
return None |
298 |
|
299 |
def _getNodeContent(self, node): |
300 |
""" |
301 |
Return the content of the node, loading it from the cache. |
302 |
""" |
303 |
|
304 |
from cStringIO import StringIO |
305 |
|
306 |
location = self._getCachedContentLocation(node) |
307 |
if location: |
308 |
return file(location) |
309 |
else: |
310 |
return StringIO('') |
311 |
|
312 |
def _getNodeSize(self, node): |
313 |
""" |
314 |
Return the content of the node, loading it from the cache. |
315 |
""" |
316 |
|
317 |
location = self._getCachedContentLocation(node) |
318 |
if location: |
319 |
return stat(location).st_size |
320 |
else: |
321 |
return None |
322 |
|
323 |
def _getNodeEntries(self, node): |
324 |
""" |
325 |
Generate the the immediate child entries of a directory at given |
326 |
revision, in alpha order. |
327 |
""" |
328 |
|
329 |
from cache import _actionmap |
330 |
|
331 |
# Loop over nodes touched before given rev that falls in the |
332 |
# given path. We effectively want to look at the whole subtree, |
333 |
# because when a child is a directory we annotate it with the |
334 |
# latest change happened below that, instead with the revision |
335 |
# that actually touched the directory itself. |
336 |
|
337 |
cursor = self.db.cursor() |
338 |
path = node.path.strip('/') |
339 |
cursor.execute("SELECT rev, path, change, base_path " |
340 |
" FROM node_change " |
341 |
"WHERE ((LENGTH(rev)<LENGTH(%s)) OR " |
342 |
" (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) " |
343 |
" AND (path GLOB %s OR base_path GLOB %s) " |
344 |
"ORDER BY -LENGTH(rev),rev DESC,path ", |
345 |
(node.rev, node.rev, node.rev, path+'*', path+'*')) |
346 |
|
347 |
if path: |
348 |
path += '/' |
349 |
|
350 |
done = {} |
351 |
for entry in cursor: |
352 |
rev, subtreepath, change, oldpath = entry |
353 |
if oldpath and oldpath.startswith(path): |
354 |
subpath = oldpath[len(path):].lstrip('/') |
355 |
elif subtreepath.startswith(path): |
356 |
subpath = subtreepath[len(path):].lstrip('/') |
357 |
else: |
358 |
# The GLOB above may return either entries in the |
359 |
# directory or entries in other directories (usually |
360 |
# the one that contains the folder we are listing) |
361 |
# that share a common chunk of the name with the |
362 |
# directory itself. |
363 |
continue |
364 |
|
365 |
if '/' in subpath: |
366 |
child, rest = subpath.split('/', 1) |
367 |
else: |
368 |
child, rest = subpath, None |
369 |
|
370 |
if not child or child in done: |
371 |
continue |
372 |
|
373 |
done[child] = True |
374 |
|
375 |
# Return the node only if the entry is not a direct child |
376 |
# of the directory, otherwise only if it's not a deletion |
377 |
# or a rename. |
378 |
if rest or not ((_actionmap[change]==Changeset.MOVE |
379 |
and oldpath and oldpath.startswith(path)) or |
380 |
_actionmap[change]==Changeset.DELETE): |
381 |
yield self.get_node(path + child, rev) |
382 |
|
383 |
def _getNodeLastModified(self, node): |
384 |
""" |
385 |
Return the timestamp of the last modification to the given node. |
386 |
""" |
387 |
|
388 |
location = self._getCachedContentLocation(node) |
389 |
if location: |
390 |
return stat(location).st_mtime |
391 |
else: |
392 |
return 0 |
393 |
|
394 |
def _getPathHistory(self, path, rev=None, limit=None): |
395 |
""" |
396 |
Iterate over the nodes that compose the history of the given |
397 |
path not newer than rev. |
398 |
""" |
399 |
|
400 |
from trac.versioncontrol.cache import _kindmap, _actionmap |
401 |
|
402 |
rev = self.normalize_rev(rev) |
403 |
|
404 |
# Start with the concrete history, if present |
405 |
if self.__history and rev>self.__history_start: |
406 |
node = None |
407 |
for cs in reversed(self.__history): |
408 |
if cs.rev > rev: |
409 |
continue |
410 |
node = cs.get_node(path) |
411 |
if node: |
412 |
yield node |
413 |
if limit: |
414 |
limit -= 1 |
415 |
if limit==0: |
416 |
break |
417 |
# Expand renames |
418 |
if node.change == Changeset.MOVE: |
419 |
for node in self._getPathHistory(node.oldpath, |
420 |
node.rev-1, limit): |
421 |
yield node |
422 |
if limit: |
423 |
limit -= 1 |
424 |
if node is not None: |
425 |
rev = node.rev-1 |
426 |
|
427 |
# Keep going with the cache stored in the DB |
428 |
kind = self._getNodeKind(path, rev) |
429 |
cursor = self.db.cursor() |
430 |
|
431 |
path = path.rstrip('/') |
432 |
if kind == Node.DIRECTORY: |
433 |
revdone = {} |
434 |
cursor.execute("SELECT rev,kind,change,base_path" |
435 |
" FROM node_change " |
436 |
"WHERE ((LENGTH(rev)<LENGTH(%s)) OR " |
437 |
" (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) " |
438 |
" AND (path=%s OR path GLOB %s) " |
439 |
"ORDER BY -LENGTH(rev),rev DESC ", |
440 |
(rev, rev, rev, path, path+'/*')) |
441 |
for row in cursor: |
442 |
rev, kind, change, base_path = row |
443 |
if not rev in revdone: |
444 |
revdone[rev] = True |
445 |
node = DarcsNode(path, rev, _kindmap[kind], |
446 |
_actionmap[change], self, |
447 |
oldpath=base_path) |
448 |
yield node |
449 |
if limit: |
450 |
limit -= 1 |
451 |
if limit==0: |
452 |
break |
453 |
else: |
454 |
while rev>=1: |
455 |
cursor.execute("SELECT kind,change,base_path,base_rev " |
456 |
"FROM node_change WHERE rev=%s AND path=%s", |
457 |
(rev, path)) |
458 |
base_rev = None |
459 |
for row in cursor: |
460 |
kind, change, base_path, base_rev = row |
461 |
node = DarcsNode(path, rev, _kindmap[kind], |
462 |
_actionmap[change], self, |
463 |
oldpath=base_path) |
464 |
yield node |
465 |
if limit: |
466 |
limit -= 1 |
467 |
if limit==0: |
468 |
break |
469 |
base_rev = base_rev and int(base_rev) or 0 |
470 |
# Expand renames |
471 |
if node.change == Changeset.MOVE: |
472 |
for node in self._getPathHistory(node.oldpath, |
473 |
base_rev, limit): |
474 |
yield node |
475 |
if limit: |
476 |
limit -= 1 |
477 |
|
478 |
if base_rev is None: |
479 |
rev -= 1 |
480 |
else: |
481 |
rev = base_rev |
482 |
|
483 |
def _getNodeKind(self, path, rev): |
484 |
""" |
485 |
Determine the kind of the path at given revision. |
486 |
""" |
487 |
|
488 |
# Determine if the path is really a directory, except when it's |
489 |
# already known: it is, when its name ends with a slash (a fake |
490 |
# one introduced by changesets_from_darcschanges()) or it is the |
491 |
# empty string, resulted from normalize_path('/'). |
492 |
if not path.endswith("/") and path <> "": |
493 |
cursor = self.db.cursor() |
494 |
cursor.execute("SELECT path " |
495 |
"FROM node_change " |
496 |
"WHERE ((LENGTH(rev)<LENGTH(%s)) OR " |
497 |
" (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) " |
498 |
" AND path=%s " |
499 |
"ORDER BY -LENGTH(rev),rev DESC " |
500 |
"LIMIT 1", (rev, rev, rev, path+'/')) |
501 |
if cursor.fetchone(): |
502 |
kind = Node.DIRECTORY |
503 |
else: |
504 |
kind = Node.FILE |
505 |
else: |
506 |
kind = Node.DIRECTORY |
507 |
return kind |
508 |
|
509 |
## Interface API |
510 |
|
511 |
def close(self): |
512 |
""" |
513 |
Close the connection to the repository. |
514 |
|
515 |
Darcs: no-op. |
516 |
""" |
517 |
|
518 |
def get_changeset(self, rev): |
519 |
""" |
520 |
Retrieve a Changeset object that describes the changes made in |
521 |
revision 'rev'. |
522 |
""" |
523 |
|
524 |
if not self.__history: |
525 |
youngest = self.normalize_rev(self.youngest_rev) |
526 |
youngest_cache = self.get_youngest_rev_in_cache(self.db) or '0' |
527 |
self.__history_start = int(youngest_cache) |
528 |
self.__history = self._changes('--last=%d' % |
529 |
(youngest - self.__history_start,), |
530 |
self.__history_start+1) |
531 |
rev = self.normalize_rev(rev) |
532 |
return self.__history[rev-self.__history_start-1] |
533 |
|
534 |
def get_node(self, path, rev=None): |
535 |
""" |
536 |
Retrieve a Node (directory or file) from the repository at the |
537 |
given path. If the rev parameter is specified, the version of the |
538 |
node at that revision is returned, otherwise the latest version |
539 |
of the node is returned. |
540 |
""" |
541 |
|
542 |
rev = self.normalize_rev(rev) |
543 |
path = self.normalize_path(path) |
544 |
|
545 |
if path == '': |
546 |
return DarcsNode('', rev, Node.DIRECTORY, None, self) |
547 |
|
548 |
kind = self._getNodeKind(path, rev) |
549 |
|
550 |
if kind == Node.DIRECTORY: |
551 |
cursor = self.db.cursor() |
552 |
path = path.rstrip('/') |
553 |
cursor.execute("SELECT rev,change " |
554 |
" FROM node_change " |
555 |
"WHERE ((LENGTH(rev)<LENGTH(%s)) OR " |
556 |
" (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) " |
557 |
" AND (path=%s OR path GLOB %s) " |
558 |
"ORDER BY -LENGTH(rev),rev DESC " |
559 |
"LIMIT 1", (rev, rev, rev, path, path+'/*')) |
560 |
lastchange = cursor.fetchone() |
561 |
if lastchange: |
562 |
rev, change = lastchange |
563 |
else: |
564 |
raise TracError, "No node at %r in revision %s" % (path, rev) |
565 |
node = DarcsNode(path, rev, Node.DIRECTORY, change, self) |
566 |
else: |
567 |
history = self._getPathHistory(path, rev, limit=1) |
568 |
try: |
569 |
node = history.next() |
570 |
except StopIteration: |
571 |
raise TracError, "No node at %r in revision %s" % (path, rev) |
572 |
|
573 |
return node |
574 |
|
575 |
def get_oldest_rev(self): |
576 |
""" |
577 |
Return the oldest revision stored in the repository. |
578 |
""" |
579 |
|
580 |
# If the repository is empty, return None |
581 |
if not self.__youngest_rev: |
582 |
return None |
583 |
return 1 |
584 |
|
585 |
def get_youngest_rev(self): |
586 |
""" |
587 |
Return the youngest revision in the repository. |
588 |
""" |
589 |
|
590 |
if not self.__youngest_rev: |
591 |
self._loadChangesetsIndex() |
592 |
return self.__youngest_rev |
593 |
|
594 |
def previous_rev(self, rev): |
595 |
""" |
596 |
Return the revision immediately preceding the specified revision. |
597 |
""" |
598 |
|
599 |
rev = self.normalize_rev(rev) |
600 |
if rev == 1: |
601 |
return None |
602 |
else: |
603 |
return rev-1 |
604 |
|
605 |
def next_rev(self, rev): |
606 |
""" |
607 |
Return the revision immediately following the specified revision. |
608 |
""" |
609 |
|
610 |
rev = self.normalize_rev(rev) |
611 |
if rev < self.get_youngest_rev(): |
612 |
return rev+1 |
613 |
else: |
614 |
return None |
615 |
|
616 |
def rev_older_than(self, rev1, rev2): |
617 |
""" |
618 |
Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2 |
619 |
in the revision sequence. |
620 |
""" |
621 |
|
622 |
return self.normalize_rev(rev1) < self.normalize_rev(rev2) |
623 |
|
624 |
def get_youngest_rev_in_cache(self, db): |
625 |
""" |
626 |
Return the youngest revision currently cached. |
627 |
|
628 |
For darcs, this is the last applied revision, not necessarily |
629 |
the youngest one. We are numbering darcs patches in order of |
630 |
application. |
631 |
""" |
632 |
|
633 |
cursor = db.cursor() |
634 |
cursor.execute("SELECT rev " |
635 |
"FROM revision " |
636 |
"ORDER BY -LENGTH(rev),rev DESC " # rev is a string |
637 |
"LIMIT 1") # in the database |
638 |
row = cursor.fetchone() |
639 |
return row and row[0] or None |
640 |
|
641 |
def get_path_history(self, path, rev=None, limit=None): |
642 |
""" |
643 |
Retrieve all the revisions containing this path (no newer than 'rev'). |
644 |
The result format should be the same as the one of Node.get_history() |
645 |
""" |
646 |
|
647 |
path = self.normalize_path(path) |
648 |
rev = self.normalize_rev(rev) |
649 |
for node in self._getPathHistory(path, rev, limit): |
650 |
yield (node.path, node.rev, node.change) |
651 |
|
652 |
def normalize_path(self, path): |
653 |
""" |
654 |
Return a canonical representation of path in the repos. |
655 |
""" |
656 |
|
657 |
if path.startswith('/'): |
658 |
return path[1:] |
659 |
else: |
660 |
return path |
661 |
|
662 |
def normalize_rev(self, rev): |
663 |
""" |
664 |
Return a canonical representation of a revision in the repos. |
665 |
'None' is a valid revision value and represents the youngest revision. |
666 |
""" |
667 |
|
668 |
try: |
669 |
rev = int(rev) |
670 |
except (ValueError, TypeError): |
671 |
rev = None |
672 |
if rev is None: |
673 |
rev = self.get_youngest_rev() |
674 |
elif rev > self.get_youngest_rev(): |
675 |
rev = self.get_youngest_rev() |
676 |
return rev |
677 |
|
678 |
|
679 |
class DarcsChangeset(Changeset): |
680 |
""" |
681 |
Represents a set of changes of a repository. |
682 |
""" |
683 |
|
684 |
def __init__(self, rev, patchname, message, author, date, changes, hash): |
685 |
if message: |
686 |
log = patchname + '\n' + message |
687 |
else: |
688 |
log = patchname |
689 |
Changeset.__init__(self, rev, log, author, date) |
690 |
self.patchname = patchname |
691 |
self.changes = changes |
692 |
self.hash = hash |
693 |
# fix up changes rev slot |
694 |
for c in self.changes: |
695 |
c.rev = rev |
696 |
|
697 |
def get_changes(self): |
698 |
""" |
699 |
Generator that produces a (path, kind, change, base_path, base_rev) |
700 |
""" |
701 |
|
702 |
moves = {} |
703 |
for c in self.changes: |
704 |
last = c.get_history(limit=2) |
705 |
try: |
706 |
last.next() |
707 |
basepath,baserev,basechg = last.next() |
708 |
except StopIteration: |
709 |
basepath = None |
710 |
baserev = -1 |
711 |
yield (c.path, c.kind, c.change, c.oldpath or basepath, baserev) |
712 |
|
713 |
def get_node(self, path, maybedir=False): |
714 |
""" |
715 |
Find and return the node relative to given path. |
716 |
""" |
717 |
|
718 |
for c in self.changes: |
719 |
if c.path == path or c.oldpath == path: |
720 |
return c |
721 |
if maybedir and not path.endswith('/'): |
722 |
path += '/' |
723 |
if c.path == path or c.oldpath == path: |
724 |
return c |
725 |
|
726 |
def insert_in_cache(self, cursor, kindmap, actionmap, log): |
727 |
""" |
728 |
Augment standard metadata with darcs patch hash. |
729 |
""" |
730 |
|
731 |
Changeset.insert_in_cache(self, cursor, kindmap, actionmap, log) |
732 |
cursor.execute("UPDATE revision SET hash = %s " |
733 |
"WHERE rev = %s", (self.hash, self.rev)) |
734 |
|
735 |
|
736 |
class DarcsNode(Node): |
737 |
""" |
738 |
Represent a single item changed within a Changeset. |
739 |
""" |
740 |
|
741 |
def __init__(self, path, rev, kind, change, repository, oldpath=None): |
742 |
Node.__init__(self, path, rev, kind) |
743 |
self.change = change |
744 |
self.repository = repository |
745 |
self.oldpath = oldpath |
746 |
|
747 |
def __cmp__(self, other): |
748 |
res = cmp(self.rev, other.rev) |
749 |
if res: |
750 |
return res |
751 |
res = cmp(self.path, other.path) |
752 |
if res == 0: |
753 |
if self.change==Changeset.MOVE and other.change==Changeset.DELETE: |
754 |
res = 1 |
755 |
elif self.change==Changeset.DELETE and other.change==Changeset.MOVE: |
756 |
res = -1 |
757 |
return res |
758 |
|
759 |
def get_content(self): |
760 |
""" |
761 |
Return a stream for reading the content of the node. This method |
762 |
will return None for directories. The returned object should provide |
763 |
a read([len]) function. |
764 |
""" |
765 |
|
766 |
if self.isdir: |
767 |
return None |
768 |
|
769 |
return self.repository._getNodeContent(self) |
770 |
|
771 |
def get_entries(self): |
772 |
""" |
773 |
Generator that yields the immediate child entries of a directory, in no |
774 |
particular order. If the node is a file, this method returns None. |
775 |
""" |
776 |
|
777 |
if self.isdir: |
778 |
return self.repository._getNodeEntries(self) |
779 |
|
780 |
def get_history(self, limit=None): |
781 |
""" |
782 |
Generator that yields (path, rev, chg) tuples, one for each |
783 |
revision in which the node was changed. This generator will |
784 |
follow copies and moves of a node (if the underlying version |
785 |
control system supports that), which will be indicated by the |
786 |
first element of the tuple (i.e. the path) changing. |
787 |
""" |
788 |
|
789 |
# Start with current version |
790 |
yield (self.path, self.rev, self.change) |
791 |
|
792 |
# Keep going with the previous steps, possibly following the old |
793 |
# name of the entry if this is a move. |
794 |
prevpath = self.oldpath or self.path |
795 |
prevrev = self.repository.normalize_rev(self.rev)-1 |
796 |
prevhist = self.repository.get_path_history(prevpath, prevrev, limit-1) |
797 |
for path, rev, chg in prevhist: |
798 |
yield (path, rev, chg) |
799 |
|
800 |
def get_properties(self): |
801 |
""" |
802 |
Returns a dictionary containing the properties (meta-data) of the node. |
803 |
The set of properties depends on the version control system. |
804 |
""" |
805 |
|
806 |
return {} |
807 |
|
808 |
def get_content_length(self): |
809 |
if self.isdir: |
810 |
return None |
811 |
return self.repository._getNodeSize(self) |
812 |
|
813 |
def get_content_type(self): |
814 |
if self.isdir: |
815 |
return None |
816 |
return guess_type(self.path)[0] |
817 |
|
818 |
def get_name(self): |
819 |
return split(self.path)[1] |
820 |
|
821 |
def get_last_modified(self): |
822 |
return self.repository._getNodeLastModified(self) |
823 |
|
824 |
|
825 |
def changesets_from_darcschanges(changes, repository, start_revision): |
826 |
""" |
827 |
Parse XML output of ``darcs changes``. |
828 |
|
829 |
Return a list of ``Changeset`` instances. |
830 |
""" |
831 |
|
832 |
from xml.sax import parseString, SAXException |
833 |
from xml.sax.handler import ContentHandler |
834 |
|
835 |
class DarcsXMLChangesHandler(ContentHandler): |
836 |
def __init__(self): |
837 |
self.changesets = [] |
838 |
self.index = start_revision-1 |
839 |
self.current = None |
840 |
self.current_field = [] |
841 |
|
842 |
def startElement(self, name, attributes): |
843 |
if name == 'patch': |
844 |
self.current = {} |
845 |
self.current['author'] = attributes['author'] |
846 |
date = attributes['date'] |
847 |
# 20040619130027 |
848 |
y = int(date[:4]) |
849 |
m = int(date[4:6]) |
850 |
d = int(date[6:8]) |
851 |
hh = int(date[8:10]) |
852 |
mm = int(date[10:12]) |
853 |
ss = int(date[12:14]) |
854 |
unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone |
855 |
self.current['date'] = unixtime |
856 |
self.current['comment'] = '' |
857 |
self.current['hash'] = attributes['hash'] |
858 |
self.current['entries'] = [] |
859 |
elif name in ['name', 'comment', 'add_file', 'add_directory', |
860 |
'remove_directory', 'modify_file', 'remove_file']: |
861 |
self.current_field = [] |
862 |
elif name == 'move': |
863 |
self.old_name = attributes['from'] |
864 |
self.new_name = attributes['to'] |
865 |
|
866 |
def endElement(self, name): |
867 |
if name == 'patch': |
868 |
# Sort the paths to make tests easier |
869 |
self.current['entries'].sort() |
870 |
self.index += 1 |
871 |
cset = DarcsChangeset(self.index, |
872 |
self.current['name'], |
873 |
self.current['comment'], |
874 |
self.current['author'], |
875 |
self.current['date'], |
876 |
self.current['entries'], |
877 |
self.current['hash']) |
878 |
self.changesets.append(cset) |
879 |
self.current = None |
880 |
elif name in ['name', 'comment']: |
881 |
self.current[name] = ''.join(self.current_field) |
882 |
elif name == 'move': |
883 |
kind = None |
884 |
for cs in reversed(self.changesets): |
885 |
node = cs.get_node(self.old_name, maybedir=True) |
886 |
if node: |
887 |
kind = node.kind |
888 |
break |
889 |
if kind is None: |
890 |
kind = repository._getNodeKind(self.old_name, self.index) |
891 |
if kind == Node.DIRECTORY: |
892 |
self.new_name += '/' |
893 |
self.old_name += '/' |
894 |
entry = DarcsNode(self.new_name, None, kind, Changeset.MOVE, |
895 |
repository, self.old_name) |
896 |
self.current['entries'].append(entry) |
897 |
elif name in ['add_file', 'add_directory', 'modify_file', |
898 |
'remove_file', 'remove_directory']: |
899 |
path = ''.join(self.current_field).strip() |
900 |
change = { 'add_file': Changeset.ADD, |
901 |
'add_directory': Changeset.ADD, |
902 |
'modify_file': Changeset.EDIT, |
903 |
'remove_file': Changeset.DELETE, |
904 |
'remove_directory': Changeset.DELETE |
905 |
}[name] |
906 |
isdir = name in ('add_directory', 'remove_directory') |
907 |
kind = isdir and Node.DIRECTORY or Node.FILE |
908 |
# Eventually add one final '/' to identify directories. |
909 |
# This is because Trac brings around simple tuples at times, |
910 |
# that cannot carry that flag with them. |
911 |
if isdir: |
912 |
path += '/' |
913 |
entry = DarcsNode(path, None, kind, change, repository) |
914 |
self.current['entries'].append(entry) |
915 |
|
916 |
def characters(self, data): |
917 |
self.current_field.append(data) |
918 |
|
919 |
handler = DarcsXMLChangesHandler() |
920 |
try: |
921 |
parseString(changes, handler) |
922 |
except SAXException, le: |
923 |
raise TracError('Unable to parse "darcs changes" output: ' + str(le)) |
924 |
|
925 |
return handler.changesets |
926 |
|
927 |
class DarcsCachedRepository(CachedRepository): |
928 |
""" |
929 |
Darcs version of the cached repository, that serves DarcsCachedChangesets |
930 |
""" |
931 |
|
932 |
def get_changeset(self, rev): |
933 |
if not self.synced: |
934 |
self.sync() |
935 |
self.synced = 1 |
936 |
return DarcsCachedChangeset(self.repos.normalize_rev(rev), self.db, |
937 |
self.authz) |
938 |
|
939 |
class DarcsCachedChangeset(CachedChangeset): |
940 |
""" |
941 |
Darcs version of the CachedChangeset that knows about the hash. |
942 |
""" |
943 |
|
944 |
def __init__(self, rev, db, authz=None): |
945 |
CachedChangeset.__init__(self, rev, db, authz) |
946 |
cursor = self.db.cursor() |
947 |
cursor.execute("SELECT hash FROM revision " |
948 |
"WHERE rev=%s", (rev,)) |
949 |
row = cursor.fetchone() |
950 |
if row: |
951 |
self.hash = row[0] |