Edgewall Software

Ticket #257: perforce.py

File perforce.py, 14.6 KB (added by thomas.tressieres@…, 6 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