1
2
3
4 """
5 Version Control System functionality.
6 """
7
8 import re
9 import os
10 import sys
11
12 from moap.util import util, log
13
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
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
60 -class 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
68 self.path = path
69 if not path:
70 self.path = os.getcwd()
71
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
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
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
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
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
160
161
162 changeMatcher = re.compile(
163 '^\@\@\s+'
164 '(-)(\d+),?(\d*)'
165 '\s+'
166 '(\+)(\d+),?(\d*)'
167 '\s+\@\@'
168 )
169
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
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
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
198
199
200
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
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
214
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
226
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
237 i -= 1
238
239 i += 1
240
241 log.debug('vcs', '%d files changed' % len(changes.keys()))
242 return changes
243
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 @rtype: dict of str -> list of str
250 @returns: dict of path -> list of property names
251 """
252 log.info('vcs',
253 "subclass %r should implement getPropertyChanges" % self.__class__)
254
256 """
257 Get a list of paths newly added under the given path.
258
259 @rtype: list of str
260 @returns: list of paths
261 """
262 log.info('vcs',
263 "subclass %r should implement getAdded" % self.__class__)
264
266 """
267 Get a list of paths deleted under the given path.
268
269 @rtype: list of str
270 @returns: list of paths
271 """
272 log.info('vcs',
273 "subclass %r should implement getDeleted" % self.__class__)
274
275
277 """
278 Update the given path to the latest version.
279 """
280 raise NotImplementedError
281
283 """
284 Generic exception for a failed VCS operation.
285 """
286 pass
287