1 # Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
2 # W. Trevor King <wking@drexel.edu>
4 # This file is part of Bugs Everywhere.
6 # Bugs Everywhere is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the
8 # Free Software Foundation, either version 2 of the License, or (at your
9 # option) any later version.
11 # Bugs Everywhere is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
21 .. _Darcs: http://darcs.net/
29 import time # work around http://mercurial.selenic.com/bts/issue618
31 try: # import core module, Python >= 2.5
32 from xml.etree import ElementTree
33 except ImportError: # look for non-core module
34 from elementtree import ElementTree
35 from xml.sax.saxutils import unescape
40 if libbe.TESTING == True:
48 class Darcs(base.VCS):
49 """:class:`base.VCS` implementation for Darcs.
54 def __init__(self, *args, **kwargs):
55 base.VCS.__init__(self, *args, **kwargs)
57 self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
59 def _vcs_version(self):
60 status,output,error = self._u_invoke_client('--version')
63 def version_cmp(self, *args):
64 """Compare the installed Darcs version `V_i` with another version
65 `V_o` (given in `*args`). Returns
76 >>> d = Darcs(repo='.')
77 >>> d._version = '2.3.1 (release)'
78 >>> d.version_cmp(2,3,1)
80 >>> d.version_cmp(2,3,2)
82 >>> d.version_cmp(2,3,0)
86 >>> d._version = '2.0.0pre2'
87 >>> d._parsed_version = None
90 >>> d.version_cmp(2,0,1)
91 Traceback (most recent call last):
93 NotImplementedError: Cannot parse non-integer portion "0pre2" of Darcs version "2.0.0pre2"
95 if not hasattr(self, '_parsed_version') \
96 or self._parsed_version == None:
97 num_part = self.version().split(' ')[0]
98 self._parsed_version = []
99 for num in num_part.split('.'):
101 self._parsed_version.append(int(num))
102 except ValueError, e:
103 self._parsed_version.append(num)
104 for current,other in zip(self._parsed_version, args):
105 if type(current) != types.IntType:
106 raise NotImplementedError(
107 'Cannot parse non-integer portion "%s" of Darcs version "%s"'
108 % (current, self.version()))
109 c = cmp(current,other)
114 def _vcs_get_user_id(self):
115 # following http://darcs.net/manual/node4.html#SECTION00410030000000000000
116 # as of June 22th, 2010
117 if self.repo == None:
119 for pref_file in ['author', 'email']:
120 for prefs_dir in [os.path.join(self.repo, '_darcs', 'prefs'),
121 os.path.expanduser(os.path.join('~', '.darcs'))]:
122 if prefs_dir == None:
124 pref_path = os.path.join(prefs_dir, pref_file)
125 if os.path.exists(pref_path):
126 return self._vcs_get_file_contents(pref_path).strip()
127 for env_variable in ['DARCS_EMAIL', 'EMAIL']:
128 if env_variable in os.environ:
129 return os.environ[env_variable]
132 def _vcs_detect(self, path):
133 if self._u_search_parent_directories(path, "_darcs") != None :
137 def _vcs_root(self, path):
138 """Find the root of the deepest repository containing path."""
139 # Assume that nothing funny is going on; in particular, that we aren't
140 # dealing with a bare repo.
141 if os.path.isdir(path) != True:
142 path = os.path.dirname(path)
143 darcs_dir = self._u_search_parent_directories(path, '_darcs')
144 if darcs_dir == None:
146 return os.path.dirname(darcs_dir)
148 def _vcs_init(self, path):
149 self._u_invoke_client('init', cwd=path)
151 def _vcs_destroy(self):
152 vcs_dir = os.path.join(self.repo, '_darcs')
153 if os.path.exists(vcs_dir):
154 shutil.rmtree(vcs_dir)
156 def _vcs_add(self, path):
157 if os.path.isdir(path):
159 if self.version_cmp(0, 9, 10) == 1:
160 self._u_invoke_client('add', '--boring', path)
161 else: # really old versions <= 0.9.10 lack --boring
162 self._u_invoke_client('add', path)
164 def _vcs_remove(self, path):
165 if not os.path.isdir(self._u_abspath(path)):
166 os.remove(os.path.join(self.repo, path)) # darcs notices removal
168 def _vcs_update(self, path):
169 self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
170 pass # darcs notices changes
172 def _vcs_get_file_contents(self, path, revision=None):
174 return base.VCS._vcs_get_file_contents(self, path, revision)
175 if self.version_cmp(2, 0, 0) == 1:
176 status,output,error = self._u_invoke_client( \
177 'show', 'contents', '--patch', revision, path)
179 # Darcs versions < 2.0.0pre2 lack the 'show contents' command
181 patch = self._diff(revision, path=path, unicode_output=False)
183 # '--output -' to be supported in GNU patch > 2.5.9
184 # but that hasn't been released as of June 30th, 2009.
186 # Rewrite path to status before the patch we want
187 args=['patch', '--reverse', path]
188 status,output,error = self._u_invoke(args, stdin=patch)
190 if os.path.exists(os.path.join(self.repo, path)) == True:
191 contents = base.VCS._vcs_get_file_contents(self, path)
195 # Now restore path to it's current incarnation
197 status,output,error = self._u_invoke(args, stdin=patch)
200 def _vcs_path(self, id, revision):
201 return self._u_find_id(id, revision)
203 def _vcs_isdir(self, path, revision):
204 if self.version_cmp(2, 3, 1) == 1:
205 # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com
206 # * add versioned show files functionality (darcs show files -p 'some patch')
207 status,output,error = self._u_invoke_client( \
208 'show', 'files', '--no-files', '--patch', revision)
209 children = output.rstrip('\n').splitlines()
211 children = [self._u_rel_path(c, rpath) for c in children]
215 raise NotImplementedError(
216 'Darcs versions <= 2.3.1 lack the --patch option for "show files"')
218 def _vcs_listdir(self, path, revision):
219 if self.version_cmp(2, 3, 1) == 1:
220 # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com
221 # * add versioned show files functionality (darcs show files -p 'some patch')
222 # Wed Dec 9 05:42:21 EST 2009 Luca Molteni <volothamp@gmail.com>
223 # * resolve issue835 show file with file directory arguments
224 path = path.rstrip(os.path.sep)
225 status,output,error = self._u_invoke_client( \
226 'show', 'files', '--patch', revision, path)
227 files = output.rstrip('\n').splitlines()
229 descendents = [self._u_rel_path(f, path) for f in files
232 rel_files = [self._u_rel_path(f, path) for f in files]
233 descendents = [f for f in rel_files
234 if f != '.' and not f.startswith('..')]
235 return [f for f in descendents if f.count(os.path.sep) == 0]
236 # Darcs versions <= 2.3.1 lack the --patch option for 'show files'
237 raise NotImplementedError
239 def _vcs_commit(self, commitfile, allow_empty=False):
240 id = self.get_user_id()
241 if id == None or '@' not in id:
242 id = '%s <%s@invalid.com>' % (id, id)
243 args = ['record', '--all', '--author', id, '--logfile', commitfile]
244 status,output,error = self._u_invoke_client(*args)
245 empty_strings = ['No changes!']
246 # work around http://mercurial.selenic.com/bts/issue618
247 if self._u_any_in_string(empty_strings, output) == True \
248 and len(self.__updated) > 0:
250 for path in self.__updated:
251 os.utime(os.path.join(self.repo, path), None)
252 status,output,error = self._u_invoke_client(*args)
255 if self._u_any_in_string(empty_strings, output) == True:
256 if allow_empty == False:
257 raise base.EmptyCommit()
258 # note that darcs does _not_ make an empty revision.
259 # this returns the last non-empty revision id...
260 revision = self._vcs_revision_id(-1)
262 revline = re.compile("Finished recording patch '(.*)'")
263 match = revline.search(output)
264 assert match != None, output+error
265 assert len(match.groups()) == 1
266 revision = match.groups()[0]
269 def _revisions(self):
271 Return a list of revisions in the repository.
273 status,output,error = self._u_invoke_client('changes', '--xml')
275 xml_str = output.encode('unicode_escape').replace(r'\n', '\n')
276 element = ElementTree.XML(xml_str)
277 assert element.tag == 'changelog', element.tag
278 for patch in element.getchildren():
279 assert patch.tag == 'patch', patch.tag
280 for child in patch.getchildren():
281 if child.tag == 'name':
282 text = unescape(unicode(child.text).decode('unicode_escape').strip())
283 revisions.append(text)
287 def _vcs_revision_id(self, index):
288 revisions = self._revisions()
291 return revisions[index-1]
293 return revisions[index]
299 def _diff(self, revision, path=None, unicode_output=True):
300 revisions = self._revisions()
301 i = revisions.index(revision)
302 args = ['diff', '--unified']
303 if i+1 < len(revisions):
304 next_rev = revisions[i+1]
305 args.extend(['--from-patch', next_rev])
308 kwargs = {'unicode_output':unicode_output}
309 status,output,error = self._u_invoke_client(
313 def _parse_diff(self, diff_text):
314 """_parse_diff(diff_text) -> (new,modified,removed)
316 `new`, `modified`, and `removed` are lists of files.
320 Mon Jan 18 15:19:30 EST 2010 None <None@invalid.com>
322 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified
323 --- old-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500
324 +++ new-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500
326 -some value to be modified
327 \ No newline at end of file
329 \ No newline at end of file
330 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved
331 --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500
332 +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500
334 -this entry will be moved
335 \ No newline at end of file
336 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2
337 --- old-BEtestgQtDuD/.be/dir/bugs/moved2 1969-12-31 19:00:00.000000000 -0500
338 +++ new-BEtestgQtDuD/.be/dir/bugs/moved2 2010-01-18 15:19:30.000000000 -0500
340 +this entry will be moved
341 \ No newline at end of file
342 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new
343 --- old-BEtestgQtDuD/.be/dir/bugs/new 1969-12-31 19:00:00.000000000 -0500
344 +++ new-BEtestgQtDuD/.be/dir/bugs/new 2010-01-18 15:19:30.000000000 -0500
347 \ No newline at end of file
348 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed
349 --- old-BEtestgQtDuD/.be/dir/bugs/removed 2010-01-18 15:19:30.000000000 -0500
350 +++ new-BEtestgQtDuD/.be/dir/bugs/removed 1969-12-31 19:00:00.000000000 -0500
352 -this entry will be deleted
353 \ No newline at end of file
359 lines = diff_text.splitlines()
360 repodir = os.path.basename(self.repo) + os.path.sep
362 while i < len(lines):
363 line = lines[i]; i += 1
364 if not line.startswith('diff '):
366 file_a,file_b = line.split()[-2:]
367 assert file_a.startswith('old-'), \
368 'missformed file_a %s' % file_a
369 assert file_b.startswith('new-'), \
370 'missformed file_a %s' % file_b
372 assert file_b[4:] == file, \
373 'diff file missmatch %s != %s' % (file_a, file_b)
374 assert file.startswith(repodir), \
375 'missformed file_a %s' % file_a
376 file = file[len(repodir):]
379 line = lines[i]; i += 1
380 assert line.startswith('--- old-'), \
381 'missformed "---" line %s' % line
382 time_a = line.split('\t')[1]
383 line = lines[i]; i += 1
384 assert line.startswith('+++ new-'), \
385 'missformed "+++" line %s' % line
386 time_b = line.split('\t')[1]
387 zero_time = time.strftime('%Y-%m-%d %H:%M:%S.000000000 ',
389 # note that zero_time is missing the trailing timezone offset
390 if time_a.startswith(zero_time):
392 elif time_b.startswith(zero_time):
395 modified.append(file)
396 return (new,modified,removed)
398 def _vcs_changed(self, revision):
399 return self._parse_diff(self._diff(revision))
402 if libbe.TESTING == True:
403 base.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__])
405 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
406 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])