source: trunk/moap/vcs/vcs.py @ 344

Revision 344, 8.4 KB checked in by thomas, 5 years ago (diff)
  • moap/vcs/vcs.py: Add getPropertyChanges() to get paths that have changed properties.
  • moap/test/test_vcs_svn.py:
  • moap/vcs/svn.py: Implement it for svn.
  • moap/command/cl.py: Use it in moap cl prep so that we allow commenting on property changes. Fixes #286.
  • Property property set to property
Line 
1# -*- Mode: Python -*-
2# vi:si:et:sw=4:sts=4:ts=4
3
4"""
5Version Control System functionality.
6"""
7
8import re
9import os
10import sys
11
12from moap.util import util, log
13
14def getNames():
15    """
16    Returns a sorted list of VCS names.
17    """
18    moduleNames = util.getPackageModules('moap.vcs', ignore=['vcs', ])
19    modules = [util.namedModule('moap.vcs.%s' % s) for s in moduleNames]
20    names = [m.VCSClass.name for m in modules]
21    names.sort()
22    return names
23
24def detect(path=None):
25    """
26    Detect which version control system is being used in the source tree.
27
28    @returns: an instance of a subclass of L{VCS}, or None.
29    """
30    log.debug('vcs', 'detecting VCS in %s' % path)
31    if not path:
32        path = os.getcwd()
33    systems = util.getPackageModules('moap.vcs', ignore=['vcs', ])
34    log.debug('vcs', 'trying vcs modules %r' % systems)
35
36    for s in systems:
37        m = util.namedModule('moap.vcs.%s' % s)
38
39        try:
40            ret = m.detect(path)
41        except AttributeError:
42                sys.stderr.write('moap.vcs.%s is missing detect()\n' % s)
43                continue
44
45        if ret:
46            try:
47                o = m.VCSClass(path)
48            except AttributeError:
49                sys.stderr.write('moap.vcs.%s is missing VCSClass()\n' % s)
50                continue
51
52            log.debug('vcs', 'detected VCS %s' % s)
53
54            return o
55        log.debug('vcs', 'did not find %s' % s)
56
57    return None
58   
59# FIXME: add stdout and stderr, so all spawned commands output there instead
60class VCS(log.Loggable):
61    """
62    cvar path: the path to the top of the source tree
63    """
64    name = 'Some Version Control System'
65    logCategory = 'VCS'
66
67    def __init__(self, path=None):
68        self.path = path
69        if not path:
70            self.path = os.getcwd()
71
72    def getNotIgnored(self):
73        """
74        @return: list of paths unknown to the VCS, relative to the base path
75        """
76        raise NotImplementedError
77
78    def ignore(self, paths, commit=True):
79        """
80        Make the VCS ignore the given list of paths.
81
82        @param paths:  list of paths, relative to the checkout directory
83        @type  paths:  list of str
84        @param commit: if True, commit the ignore updates.
85        @type  commit: boolean
86        """
87        raise NotImplementedError
88
89    def commit(self, paths, message):
90        """
91        Commit the given list of paths, with the given message.
92        Note that depending on the VCS, parents that were just added
93        may need to be commited as well.
94
95        @type paths:   list
96        @type message: str
97
98        @rtype: bool
99        """
100
101    def createTree(self, paths):
102        """
103        Given the list of paths, create a dict of parentPath -> [child, ...]
104        If the path is in the root of the repository, parentPath will be ''
105
106        @rtype: dict of str -> list of str
107        """
108        result = {}
109
110        if not paths:
111            return result
112
113        for p in paths:
114            # os.path.basename('test/') returns '', so strip possibly trailing /
115            if p.endswith(os.path.sep): p = p[:-1]
116            base = os.path.basename(p)
117            dirname = os.path.dirname(p)
118            if not dirname in result.keys():
119                result[dirname] = []
120            result[dirname].append(base)
121
122        return result
123
124    def diff(self, path):
125        """
126        Return a diff for the given path.
127
128        @rtype:   str
129        @returns: the diff
130        """
131        raise NotImplementedError
132
133    def getFileMatcher(self):
134        """
135        Return an re matcher object that will expand to the file being
136        changed.
137
138        The default implementation works for CVS and SVN.
139        """
140        return re.compile('^Index: (\S+)$')
141
142    def getChanges(self, path, diff=None):
143        """
144        Get a list of changes for the given path and subpaths.
145
146        @type  diff: str
147        @param diff: the diff to use instead of a local vcs diff
148                     (only useful for testing)
149
150        @returns: dict of path -> list of (oldLine, oldCount, newLine, newCount)
151        """
152        if not diff:
153            self.debug('getting changes from diff in %s' % path)
154            diff = self.diff(path)
155
156        changes = {}
157        fileMatcher = self.getFileMatcher()
158
159        # cvs diff can put a function name after the final @@ pair
160        # svn diff on a one-line change in a one-line file looks like this:
161        # @@ -1 +1 @@
162        changeMatcher = re.compile(
163            '^\@\@\s+'         # start of line
164            '(-)(\d+),?(\d*)'  # -x,y or -x
165            '\s+'
166            '(\+)(\d+),?(\d*)'
167            '\s+\@\@'          # end of line
168        )
169        # We rstrip so that we don't end up with a dangling '' line
170        lines = diff.rstrip('\n').split("\n")
171        self.debug('diff is %d lines' % len(lines))
172        for i in range(len(lines)):
173            fm = fileMatcher.search(lines[i])
174            if fm:
175                # found a file being diffed, now get changes
176                path = fm.expand('\\1')
177                self.debug('Found file %s with deltas on line %d' % (
178                    path, i + 1))
179                changes[path] = []
180                i += 1
181                while i < len(lines) and not fileMatcher.search(lines[i]):
182                    self.log('Looking at line %d for file match' % (i + 1))
183                    m = changeMatcher.search(lines[i])
184                    if m:
185                        self.debug('Found change on line %d' % (i + 1))
186                        oldLine = int(m.expand('\\2'))
187                        # oldCount can be missing, which means it's 1
188                        c = m.expand('\\3')
189                        if not c: c = '1'
190                        oldCount = int(c)
191                        newLine = int(m.expand('\\5'))
192                        c = m.expand('\\6')
193                        if not c: c = '1'
194                        newCount = int(c)
195                        i += 1
196
197                        # the diff has 3 lines of context by default
198                        # if a line was added/removed at the beginning or end,
199                        # that context is not always there
200                        # so we need to parse each non-changeMatcher line
201                        block = []
202                        while i < len(lines) \
203                            and not changeMatcher.search(lines[i]) \
204                            and not fileMatcher.search(lines[i]):
205                            block.append(lines[i])
206                            i += 1
207
208                        # now we have the whole block
209                        self.log('Found change block of %d lines at line %d' % (
210                            len(block), i - len(block) + 1))
211
212                        for line in block:
213                            # starting non-change lines add to Line and
214                            # subtract from Count
215                            if line[0] == ' ':
216                                oldLine += 1
217                                newLine += 1
218                                oldCount -= 1
219                                newCount -= 1
220                            else:
221                                break
222
223                        block.reverse()
224                        for line in block:
225                            # trailing non-change lines subtract from Count
226                            # line can be empty
227                            if line and line[0] == ' ':
228                                oldCount -= 1
229                                newCount -= 1
230                            else:
231                                break
232
233                        changes[path].append(
234                            (oldLine, oldCount, newLine, newCount))
235
236                        # we're at a change line, so go back
237                        i -= 1
238
239                    i += 1
240
241        log.debug('vcs', '%d files changed' % len(changes.keys()))
242        return changes
243
244    def getPropertyChanges(self, path):
245        """
246        Get a list of property changes for the given path and subpaths.
247        These are metadata changes to files, not content changes.
248
249        @returns: list of path
250        """
251        log.info('vcs', 
252            "subclass %r should implement getPropertyChanges" % self.__class__)
253 
254    def update(self, path):
255        """
256        Update the given path to the latest version.
257        """
258        raise NotImplementedError
259
260class VCSException(Exception):
261    """
262    Generic exception for a failed VCS operation.
263    """
264    pass
Note: See TracBrowser for help on using the repository browser.