1 # Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
2 # W. Trevor King <wking@drexel.edu>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 .. _Darcs: http://darcs.net/
28 import time # work around http://mercurial.selenic.com/bts/issue618
30 try: # import core module, Python >= 2.5
31 from xml.etree import ElementTree
32 except ImportError: # look for non-core module
33 from elementtree import ElementTree
34 from xml.sax.saxutils import unescape
39 if libbe.TESTING == True:
47 class Darcs(base.VCS):
48 """:class:`base.VCS` implementation for Darcs.
53 def __init__(self, *args, **kwargs):
54 base.VCS.__init__(self, *args, **kwargs)
56 self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
58 def _vcs_version(self):
59 status,output,error = self._u_invoke_client('--version')
62 def version_cmp(self, *args):
63 """Compare the installed Darcs version `V_i` with another version
64 `V_o` (given in `*args`). Returns
75 >>> d = Darcs(repo='.')
76 >>> d._vcs_version = lambda : "2.3.1 (release)"
77 >>> d.version_cmp(2,3,1)
79 >>> d.version_cmp(2,3,2)
81 >>> d.version_cmp(2,3,0)
85 >>> d._vcs_version = lambda : "2.0.0pre2"
86 >>> d._parsed_version = None
89 >>> d.version_cmp(2,0,1)
90 Traceback (most recent call last):
92 NotImplementedError: Cannot parse non-integer portion "0pre2" of Darcs version "2.0.0pre2"
94 if not hasattr(self, '_parsed_version') \
95 or self._parsed_version == None:
96 num_part = self._vcs_version().split(' ')[0]
97 self._parsed_version = []
98 for num in num_part.split('.'):
100 self._parsed_version.append(int(num))
101 except ValueError, e:
102 self._parsed_version.append(num)
103 for current,other in zip(self._parsed_version, args):
104 if type(current) != types.IntType:
105 raise NotImplementedError(
106 'Cannot parse non-integer portion "%s" of Darcs version "%s"'
107 % (current, self._vcs_version()))
108 c = cmp(current,other)
113 def _vcs_get_user_id(self):
114 # following http://darcs.net/manual/node4.html#SECTION00410030000000000000
115 # as of June 29th, 2009
116 if self.repo == None:
118 darcs_dir = os.path.join(self.repo, '_darcs')
119 if darcs_dir != None:
120 for pref_file in ['author', 'email']:
121 pref_path = os.path.join(darcs_dir, 'prefs', pref_file)
122 if os.path.exists(pref_path):
123 return self.get_file_contents(pref_path)
124 for env_variable in ['DARCS_EMAIL', 'EMAIL']:
125 if env_variable in os.environ:
126 return os.environ[env_variable]
129 def _vcs_detect(self, path):
130 if self._u_search_parent_directories(path, "_darcs") != None :
134 def _vcs_root(self, path):
135 """Find the root of the deepest repository containing path."""
136 # Assume that nothing funny is going on; in particular, that we aren't
137 # dealing with a bare repo.
138 if os.path.isdir(path) != True:
139 path = os.path.dirname(path)
140 darcs_dir = self._u_search_parent_directories(path, '_darcs')
141 if darcs_dir == None:
143 return os.path.dirname(darcs_dir)
145 def _vcs_init(self, path):
146 self._u_invoke_client('init', cwd=path)
148 def _vcs_destroy(self):
149 vcs_dir = os.path.join(self.repo, '_darcs')
150 if os.path.exists(vcs_dir):
151 shutil.rmtree(vcs_dir)
153 def _vcs_add(self, path):
154 if os.path.isdir(path):
156 self._u_invoke_client('add', path)
158 def _vcs_remove(self, path):
159 if not os.path.isdir(self._u_abspath(path)):
160 os.remove(os.path.join(self.repo, path)) # darcs notices removal
162 def _vcs_update(self, path):
163 self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
164 pass # darcs notices changes
166 def _vcs_get_file_contents(self, path, revision=None):
168 return base.VCS._vcs_get_file_contents(self, path, revision)
169 if self.version_cmp(2, 0, 0) == 1:
170 status,output,error = self._u_invoke_client( \
171 'show', 'contents', '--patch', revision, path)
173 # Darcs versions < 2.0.0pre2 lack the 'show contents' command
175 patch = self._diff(revision, path=path, unicode_output=False)
177 # '--output -' to be supported in GNU patch > 2.5.9
178 # but that hasn't been released as of June 30th, 2009.
180 # Rewrite path to status before the patch we want
181 args=['patch', '--reverse', path]
182 status,output,error = self._u_invoke(args, stdin=patch)
184 if os.path.exists(os.path.join(self.repo, path)) == True:
185 contents = base.VCS._vcs_get_file_contents(self, path)
189 # Now restore path to it's current incarnation
191 status,output,error = self._u_invoke(args, stdin=patch)
194 def _vcs_path(self, id, revision):
195 return self._u_find_id(id, revision)
197 def _vcs_isdir(self, path, revision):
198 if self.version_cmp(2, 3, 1) == 1:
199 # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com
200 # * add versioned show files functionality (darcs show files -p 'some patch')
201 status,output,error = self._u_invoke_client( \
202 'show', 'files', '--no-files', '--patch', revision)
203 children = output.rstrip('\n').splitlines()
205 children = [self._u_rel_path(c, rpath) for c in children]
209 raise NotImplementedError(
210 'Darcs versions <= 2.3.1 lack the --patch option for "show files"')
212 def _vcs_listdir(self, path, revision):
213 if self.version_cmp(2, 3, 1) == 1:
214 # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com
215 # * add versioned show files functionality (darcs show files -p 'some patch')
216 # Wed Dec 9 05:42:21 EST 2009 Luca Molteni <volothamp@gmail.com>
217 # * resolve issue835 show file with file directory arguments
218 path = path.rstrip(os.path.sep)
219 status,output,error = self._u_invoke_client( \
220 'show', 'files', '--patch', revision, path)
221 files = output.rstrip('\n').splitlines()
223 descendents = [self._u_rel_path(f, path) for f in files
226 descendents = [self._u_rel_path(f, path) for f in files
227 if f.startswith(path)]
228 return [f for f in descendents if f.count(os.path.sep) == 0]
229 # Darcs versions <= 2.3.1 lack the --patch option for 'show files'
230 raise NotImplementedError
232 def _vcs_commit(self, commitfile, allow_empty=False):
233 id = self.get_user_id()
234 if id == None or '@' not in id:
235 id = '%s <%s@invalid.com>' % (id, id)
236 args = ['record', '--all', '--author', id, '--logfile', commitfile]
237 status,output,error = self._u_invoke_client(*args)
238 empty_strings = ['No changes!']
239 # work around http://mercurial.selenic.com/bts/issue618
240 if self._u_any_in_string(empty_strings, output) == True \
241 and len(self.__updated) > 0:
243 for path in self.__updated:
244 os.utime(os.path.join(self.repo, path), None)
245 status,output,error = self._u_invoke_client(*args)
248 if self._u_any_in_string(empty_strings, output) == True:
249 if allow_empty == False:
250 raise base.EmptyCommit()
251 # note that darcs does _not_ make an empty revision.
252 # this returns the last non-empty revision id...
253 revision = self._vcs_revision_id(-1)
255 revline = re.compile("Finished recording patch '(.*)'")
256 match = revline.search(output)
257 assert match != None, output+error
258 assert len(match.groups()) == 1
259 revision = match.groups()[0]
262 def _revisions(self):
264 Return a list of revisions in the repository.
266 status,output,error = self._u_invoke_client('changes', '--xml')
268 xml_str = output.encode('unicode_escape').replace(r'\n', '\n')
269 element = ElementTree.XML(xml_str)
270 assert element.tag == 'changelog', element.tag
271 for patch in element.getchildren():
272 assert patch.tag == 'patch', patch.tag
273 for child in patch.getchildren():
274 if child.tag == 'name':
275 text = unescape(unicode(child.text).decode('unicode_escape').strip())
276 revisions.append(text)
280 def _vcs_revision_id(self, index):
281 revisions = self._revisions()
284 return revisions[index-1]
286 return revisions[index]
292 def _diff(self, revision, path=None, unicode_output=True):
293 revisions = self._revisions()
294 i = revisions.index(revision)
295 args = ['diff', '--unified']
296 if i+1 < len(revisions):
297 next_rev = revisions[i+1]
298 args.extend(['--from-patch', next_rev])
301 kwargs = {'unicode_output':unicode_output}
302 status,output,error = self._u_invoke_client(
306 def _parse_diff(self, diff_text):
307 """_parse_diff(diff_text) -> (new,modified,removed)
309 `new`, `modified`, and `removed` are lists of files.
313 Mon Jan 18 15:19:30 EST 2010 None <None@invalid.com>
315 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified
316 --- old-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500
317 +++ new-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500
319 -some value to be modified
320 \ No newline at end of file
322 \ No newline at end of file
323 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved
324 --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500
325 +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500
327 -this entry will be moved
328 \ No newline at end of file
329 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2
330 --- old-BEtestgQtDuD/.be/dir/bugs/moved2 1969-12-31 19:00:00.000000000 -0500
331 +++ new-BEtestgQtDuD/.be/dir/bugs/moved2 2010-01-18 15:19:30.000000000 -0500
333 +this entry will be moved
334 \ No newline at end of file
335 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new
336 --- old-BEtestgQtDuD/.be/dir/bugs/new 1969-12-31 19:00:00.000000000 -0500
337 +++ new-BEtestgQtDuD/.be/dir/bugs/new 2010-01-18 15:19:30.000000000 -0500
340 \ No newline at end of file
341 diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed
342 --- old-BEtestgQtDuD/.be/dir/bugs/removed 2010-01-18 15:19:30.000000000 -0500
343 +++ new-BEtestgQtDuD/.be/dir/bugs/removed 1969-12-31 19:00:00.000000000 -0500
345 -this entry will be deleted
346 \ No newline at end of file
352 lines = diff_text.splitlines()
353 repodir = os.path.basename(self.repo) + os.path.sep
355 while i < len(lines):
356 line = lines[i]; i += 1
357 if not line.startswith('diff '):
359 file_a,file_b = line.split()[-2:]
360 assert file_a.startswith('old-'), \
361 'missformed file_a %s' % file_a
362 assert file_b.startswith('new-'), \
363 'missformed file_a %s' % file_b
365 assert file_b[4:] == file, \
366 'diff file missmatch %s != %s' % (file_a, file_b)
367 assert file.startswith(repodir), \
368 'missformed file_a %s' % file_a
369 file = file[len(repodir):]
372 line = lines[i]; i += 1
373 assert line.startswith('--- old-'), \
374 'missformed "---" line %s' % line
375 time_a = line.split('\t')[1]
376 line = lines[i]; i += 1
377 assert line.startswith('+++ new-'), \
378 'missformed "+++" line %s' % line
379 time_b = line.split('\t')[1]
380 zero_time = time.strftime('%Y-%m-%d %H:%M:%S.000000000 ',
382 # note that zero_time is missing the trailing timezone offset
383 if time_a.startswith(zero_time):
385 elif time_b.startswith(zero_time):
388 modified.append(file)
389 return (new,modified,removed)
391 def _vcs_changed(self, revision):
392 return self._parse_diff(self._diff(revision))
395 if libbe.TESTING == True:
396 base.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__])
398 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
399 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])