Edgewall Software

Ticket #257: perforce.py

File perforce.py, 14.6 KB (added by thomas.tressieres@…, 18 years ago)

second version (based on Jason Parks work)

Line 
1# -*- coding: iso8859-1 -*-
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: Jason Parks <jparks@jparks.net>
16# Thomas Tressieres <thomas.tressieres@free.fr>
17
18from __future__ import generators
19
20from trac.util import TracError
21from trac.versioncontrol import Changeset, Node, Repository
22
23import p4, os
24
25
26TmpFileName = os.tempnam() + "_perforce_output.bin"
27
28def _normalize_path(path):
29 """
30 Return a canonical representation of path in the repos.
31 """
32 return path + "..."
33
34
35class PerforceStream(object):
36 """
37 Wrapper object
38 """
39
40 def __init__(self, content):
41 self.content = content
42
43 def read(self, amt=None):
44 return self.content[:amt]
45
46
47
48class PerforceRepository(Repository):
49 """
50 Repository implementation for perforce
51 """
52
53 def __init__(self, name, authz, log, port, user, client, passwd, maxItems):
54 Repository.__init__(self, name, authz, log)
55 self.p4c = p4.P4()
56 self.p4c.port = port
57 self.p4c.user = user
58 self.p4c.client = client
59 self.p4c.password = passwd
60 self.p4c.parse_forms()
61 try:
62 self.p4c.connect()
63 # cache the first few changes
64 self.history = []
65 changes = self.p4c.run("changes", "-m", maxItems, "-s", "submitted")
66 for change in changes:
67 self.history.append(change['change'])
68
69 except self.p4c.P4Error:
70 for e in p4.errors:
71 self.log.debug(e)
72
73
74 def close(self):
75 """
76 Close the connection to the repository.
77 """
78 raise NotImplementedError
79
80
81 def get_changeset(self, rev):
82 """
83 Retrieve a Changeset object that describes the changes made in revision 'rev'.
84 """
85 #self.log.debug("*** get_changeset rev = %s" % (rev))
86 change = { }
87 try:
88 if rev != None:
89 change = self.p4c.run_describe(rev)[0]
90 else:
91 young = self.get_youngest_rev()
92 change = self.p4c.run_describe(young)[0]
93 except self.p4c.P4Error:
94 for e in p4.errors:
95 self.log.debug(e)
96 return PerforceChangeset(self.p4c, rev, change, self.log)
97
98
99 def has_node(self, path, rev):
100 """
101 Tell if there's a node at the specified (path,rev) combination.
102 """
103 #self.log.debug("*** has_node %s %s" % (path, rev))
104 try:
105 self.get_node()
106 return True
107 except TracError:
108 return False
109
110
111 def get_node(self, path, rev=None):
112 """
113 Retrieve a Node (directory or file) from the repository at the
114 given path. If the rev parameter is specified, the version of the
115 node at that revision is returned, otherwise the latest version
116 of the node is returned.
117 """
118 #self.log.debug("*** get_node path = '%s' rev = %s" % (path, rev))
119 if path != '/':
120 if path.startswith("//") == False:
121 path = path.rstrip('/')
122 path = '/' + path
123
124 if path.endswith("...") == True:
125 path2 = path.rstrip('...')
126 dir = self.p4c.run("dirs", path2)
127 else:
128 dir = self.p4c.run("dirs", path)
129
130 if len(dir) != 0:
131 kind = Node.DIRECTORY
132 else:
133 kind = Node.FILE
134 else:
135 kind = Node.DIRECTORY
136 return PerforceNode(path, rev, self.p4c, self.log, kind)
137
138
139 def get_oldest_rev(self):
140 #self.log.debug("*** get_oldest_rev rev = %s" % (self.history[-1]))
141 return self.history[-1]
142
143
144 def get_youngest_rev(self):
145 """
146 Return the youngest revision in the repository.
147 """
148 rev = self.p4c.run("changes", "-m", "1", "-s", "submitted")[0]['change']
149 #self.log.debug("*** get_youngest_rev rev = %s" % (rev))
150
151 if rev != self.history[0]:
152 count = int(rev) - int(self.history[0])
153 changes = self.p4c.run("changes", "-m", count, "-s", "submitted")
154 idx = 0
155 for change in changes:
156 num = change['change']
157 if rev != num:
158 #self.log.debug("*** inserting change %s into history at %d" % (num, idx))
159 self.history.insert(idx, num)
160 idx += 1
161 else:
162 break
163 return rev
164
165
166 def previous_rev(self, rev):
167 """
168 Return the revision immediately preceding the specified revision.
169 """
170 #self.log.debug("*** previous_rev rev = %s" % (rev))
171 idx = self.history.index(rev)
172 if idx + 1 < len(self.history):
173 return self.history[idx + 1]
174 return None
175
176
177 def next_rev(self, rev):
178 """
179 Return the revision immediately following the specified revision.
180 """
181 #self.log.debug("*** next_rev rev = %s" % (rev))
182 idx = self.history.index(rev)
183 if idx > 0:
184 return self.history[idx - 1]
185 return None
186
187
188 def rev_older_than(self, rev1, rev2):
189 """
190 Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
191 in the revision sequence.
192 """
193 #self.log.debug("rev_older_than = %s %s" % (rev1, rev2))
194 raise NotImplementedError
195
196
197 def get_path_history(self, path, rev=None, limit=None):
198 """
199 Retrieve all the revisions containing this path (no newer than 'rev').
200 The result format should be the same as the one of Node.get_history()
201 """
202 #self.log.debug("get_path_history = %s %s %s" % (path, rev, limit))
203 raise NotImplementedError
204
205
206 def normalize_path(self, path):
207 """
208 Return a canonical representation of path in the repos.
209 """
210 #self.log.debug("normalize_path = %s" % (path))
211 if path != '/':
212 if path.startswith("//") == False:
213 path = path.rstrip('/')
214 path = '/' + path
215 dir = self.p4c.run("dirs", path)
216 if len(dir) != 0:
217 kind = Node.DIRECTORY
218 else:
219 kind = Node.FILE
220 else:
221 kind = Node.DIRECTORY
222 if kind == Node.DIRECTORY:
223 return path + "/..."
224 return path
225
226
227 def normalize_rev(self, rev):
228 """
229 Return a canonical representation of a revision in the repos.
230 'None' is a valid revision value and represents the youngest revision.
231 """
232 if rev == None:
233 rev = self.get_youngest_rev()
234 elif rev > self.get_youngest_rev():
235 raise TracError, "Revision %s doesn't exist yet" % rev
236 #self.log.debug("normalize_rev = %s" % (rev))
237 return rev
238
239
240
241
242class PerforceNode(Node):
243 """
244 Represents a directory or file in the repository.
245 """
246 def __init__(self, path, rev, p4c, log, kind):
247 self.p4c = p4c
248 self.log = log
249
250 Node.__init__(self, path, rev, kind)
251
252 if self.isfile:
253 self.content = None
254 self.info = self.p4c.run("files", path)[0]
255
256
257 def _get_content(self):
258 if self.rev == None:
259 cmd = self.path + '#head'
260 else:
261 cmd = self.path + '@' + self.rev
262
263 type = self.p4c.run("fstat", cmd)
264 if type[0]['headType'].startswith('binary') == True:
265 file = self.p4c.run("print", "-o", TmpFileName, cmd)
266 f = open(TmpFileName, 'rb')
267 self.content = f.read()
268 f.close()
269 else:
270 file = self.p4c.run("print", cmd)
271 del file[0]
272 sep = '\n'
273 self.content = sep.join(file)
274 #self.log.debug("*** content = %s" % (self.content))
275 return self.content
276
277
278 def get_content(self):
279 """
280 Return a stream for reading the content of the node. This method
281 will return None for directories. The returned object should provide
282 a read([len]) function.
283 """
284 if self.isdir:
285 return None
286 return PerforceStream(self._get_content())
287
288
289 def get_entries(self):
290 """
291 Generator that yields the immediate child entries of a directory, in no
292 particular order. If the node is a file, this method returns None.
293 """
294 #self.log.debug("*** get_entries for '%s' kind = %s" % (self.path, self.kind))
295 if self.isfile:
296 return
297 path = self.path + "/*"
298 dirs = self.p4c.run("dirs", path)
299 #self.log.debug("--- dirs = '%s'" % (dirs))
300
301 for dir in dirs:
302 myDir = dir['dir'] + "..."
303 logs = self.p4c.run("fstat", myDir)
304 revs = []
305 for myLog in logs:
306 newRev = int(myLog['headChange'])
307 if not newRev in revs:
308 revs.append(newRev)
309 revs.sort()
310
311 yield PerforceNode(dir['dir'], str(revs[-1]), self.p4c, self.log, Node.DIRECTORY)
312
313 if self.path != '/':
314 files = self.p4c.run("files", path)
315 for file in files:
316 #self.log.debug("found file '%s'" % (file['depotFile']))
317 change = self.p4c.run("fstat", file['depotFile'])[0]
318 rev = change['headChange']
319 if change['headAction'] != 'delete':
320 yield PerforceNode(file['depotFile'], rev, self.p4c, self.log, Node.FILE)
321
322
323 def get_history(self, limit=None):
324 """
325 Generator that yields (path, rev, chg) tuples, one for each revision in which
326 the node was changed. This generator will follow copies and moves of a
327 node (if the underlying version control system supports that), which
328 will be indicated by the first element of the tuple (i.e. the path)
329 changing.
330 Starts with an entry for the current revision.
331 """
332 histories = []
333
334 cmd = _normalize_path(self.path)
335 #self.log.debug("*** get_history = %s %s" % (cmd, limit))
336 if self.isfile:
337 logs = self.p4c.run("filelog", "-m", str(limit), cmd)
338 #self.log.debug("*** get_history logs %s" % (logs))
339 index = 0
340 while index < len(logs[0]['rev']):
341 chg = Changeset.EDIT
342 path = self.path
343 rev = logs[0]['change'][index]
344 action = logs[0]['action'][index]
345
346 if action == 'add':
347 chg = Changeset.ADD
348 elif action == 'integrate':
349 chg = Changeset.COPY
350 elif action == 'branch':
351 chg = Changeset.COPY
352 histories.append([path, rev, chg])
353 path = logs[0]['file'][index][0]
354 chg = Changeset.EDIT
355 rev = str(int(rev) - 1)
356 elif action == 'delete':
357 chg = Changeset.DELETE
358
359 histories.append([path, rev, chg])
360 index += 1
361 else:
362 logs = self.p4c.run("fstat", cmd)
363 index = 0
364 revs = []
365 for myLog in logs:
366 newRev = myLog['headChange']
367 if newRev in revs:
368 pass
369 else:
370 revs.append(newRev)
371 revs.sort()
372 revs.reverse()
373
374 for rev in revs:
375 histories.append([self.path, rev, Changeset.EDIT])
376 index += 1
377
378 for c in histories:
379 yield tuple(c)
380
381
382 def get_properties(self):
383 """
384 Returns a dictionary containing the properties (meta-data) of the node.
385 The set of properties depends on the version control system.
386 """
387 #self.log.debug("*** get_properties = %s rev=%s" % (self.path, self.rev))
388 return { }
389
390
391 def get_content_length(self):
392 if self.isdir:
393 return None
394 type = self.p4c.run("fstat", "-Ol", self.path)
395 if type[0]['headAction'].startswith('delete') == True:
396 return 0
397 #self.log.debug("*** get_content_length = %s" % type)
398 return int(type[0]['fileSize'])
399
400
401 def get_content_type(self):
402 #self.log.debug("*** get_content_type = %s rev = %s" % (self.path, self.rev))
403 if self.isdir:
404 return None
405 change = self.p4c.run("fstat", self.path)[0]
406 if change['headType'].startswith('binary') == True:
407 return 'application/octet-stream'
408 return None
409
410
411 def get_last_modified(self):
412 #self.log.debug("*** get_last_modified = %s" % self.path)
413 return int(self.info['time'])
414
415
416
417class PerforceChangeset(Changeset):
418 """
419 Represents a set of changes of a repository.
420 """
421
422 def __init__(self, p4c, rev, change, log):
423 self.log = log
424 self.rev = rev
425 self.change = change
426 self.p4c = p4c
427 message = ""
428 author = ""
429 date = 0
430 #self.log.debug("*** changeset init = %s rev = %s" % (change, rev))
431 if len(change) != 0:
432 message = self.change['desc']
433 author = self.change['user']
434 date = int(self.change['time'])
435 Changeset.__init__(self, rev, message, author, date)
436
437 def get_changes(self):
438 """
439 Generator that produces a (path, kind, change, base_path, base_rev)
440 tuple for every change in the changeset, where change can be one of
441 Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
442 Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
443 """
444 #self.log.debug("*** get_changes = %s" % (self.change))
445 files = self.change['depotFile']
446 changes = []
447
448 index = 0
449 for file in files:
450 #rev = self.change['rev'][index]
451 rev = str(int(self.rev) - 1)
452 action = self.change['action'][index]
453 #self.log.debug("*** get_changes %s %s %s" % (file, action, rev))
454
455 if action == 'integrate':
456 filelog = self.p4c.run("filelog", "-m", "1", file)
457 action = Changeset.COPY
458 changes.append([file, Node.FILE, action, filelog[0]['file'][0][0], rev])
459 else:
460 changes.append([file, Node.FILE, action, file, rev])
461 index += 1
462
463 for c in changes:
464 yield tuple(c)
465