1 # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
2 # Ben Finney <benf@cybersource.com.au>
3 # Gianluca Montecchi <gian@grys.it>
4 # James Rowe <jnrowe@ukfsn.org>
5 # W. Trevor King <wking@drexel.edu>
7 # This file is part of Bugs Everywhere.
9 # Bugs Everywhere is free software; you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by the
11 # Free Software Foundation, either version 2 of the License, or (at your
12 # option) any later version.
14 # Bugs Everywhere is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 # General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
22 """GNU Arch_ (tla) backend.
24 .. _Arch: http://www.gnu.org/software/gnu-arch/
33 import time # work around http://mercurial.selenic.com/bts/issue618
36 import libbe.ui.util.user
37 import libbe.storage.util.config
38 from libbe.util.id import uuid_gen
39 from libbe.util.subproc import CommandError
42 if libbe.TESTING == True:
47 class CantAddFile(Exception):
48 def __init__(self, file):
50 Exception.__init__(self, "Can't automatically add file %s" % file)
52 DEFAULT_CLIENT = 'tla'
54 client = libbe.storage.util.config.get_val(
55 'arch_client', default=DEFAULT_CLIENT)
61 """:class:`base.VCS` implementation for GNU Arch.
70 _arch_paramdir = os.path.expanduser('~/.arch-params')
72 def __init__(self, *args, **kwargs):
73 base.VCS.__init__(self, *args, **kwargs)
75 self.interspersed_vcs_files = True
77 self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618
79 def _vcs_version(self):
80 status,output,error = self._u_invoke_client('--version')
81 version = '\n'.join(output.splitlines()[:2])
84 def _vcs_detect(self, path):
85 """Detect whether a directory is revision-controlled using Arch"""
86 if self._u_search_parent_directories(path, '{arch}') != None :
87 libbe.storage.util.config.set_val('arch_client', client)
91 def _vcs_init(self, path):
92 self._create_archive(path)
93 self._create_project(path)
94 self._add_project_code(path)
96 def _create_archive(self, path):
97 """Create a temporary Arch archive in the directory PATH. This
98 archive will be removed by::
100 destroy->_vcs_destroy->_remove_archive
102 # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive
103 assert self._archive_name == None
104 id = self.get_user_id()
105 name, email = libbe.ui.util.user.parse_user_id(id)
107 email = '%s@example.com' % name
108 trailer = '%s-%s' % ('bugs-everywhere-auto', uuid_gen()[0:8])
109 self._archive_name = '%s--%s' % (email, trailer)
110 self._archive_dir = '/tmp/%s' % trailer
111 self._tmp_archive = True
112 self._u_invoke_client('make-archive', self._archive_name,
113 self._archive_dir, cwd=path)
115 def _invoke_client(self, *args, **kwargs):
116 """Invoke the client on our archive.
118 assert self._archive_name != None
124 arglist = [command, '-A', self._archive_name]
125 arglist.extend(tailargs)
126 args = tuple(arglist)
127 return self._u_invoke_client(*args, **kwargs)
129 def _remove_archive(self):
130 assert self._tmp_archive == True
131 assert self._archive_dir != None
132 assert self._archive_name != None
133 os.remove(os.path.join(self._arch_paramdir,
134 '=locations', self._archive_name))
135 shutil.rmtree(self._archive_dir)
136 self._tmp_archive = False
137 self._archive_dir = False
138 self._archive_name = False
140 def _create_project(self, path):
142 Create a temporary Arch project in the directory PATH. This
143 project will be removed by
144 destroy->_vcs_destroy->_remove_project
146 # http://mwolson.org/projects/GettingStartedWithArch.html
147 # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project
148 category = 'bugs-everywhere'
151 self._project_name = '%s--%s--%s' % (category, branch, version)
152 self._invoke_client('archive-setup', self._project_name,
154 self._tmp_project = True
156 def _remove_project(self):
157 assert self._tmp_project == True
158 assert self._project_name != None
159 assert self._archive_dir != None
160 shutil.rmtree(os.path.join(self._archive_dir, self._project_name))
161 self._tmp_project = False
162 self._project_name = False
164 def _archive_project_name(self):
165 assert self._archive_name != None
166 assert self._project_name != None
167 return '%s/%s' % (self._archive_name, self._project_name)
169 def _adjust_naming_conventions(self, path):
170 """Adjust `Arch naming conventions`_ so ``.be`` is considered source
173 By default, Arch restricts source code filenames to::
177 Since our bug directory ``.be`` doesn't satisfy these conventions,
178 we need to adjust them. The conventions are specified in::
180 project-root/{arch}/=tagging-method
182 .. _Arch naming conventions:
183 http://regexps.srparish.net/tutorial-tla/naming-conventions.html
185 tagpath = os.path.join(path, '{arch}', '=tagging-method')
187 f = codecs.open(tagpath, 'r', self.encoding)
189 if line.startswith('source '):
190 lines_out.append('source ^[._=a-zA-X0-9].*$\n')
192 lines_out.append(line)
194 f = codecs.open(tagpath, 'w', self.encoding)
195 f.write(''.join(lines_out))
198 def _add_project_code(self, path):
199 # http://mwolson.org/projects/GettingStartedWithArch.html
200 # http://regexps.srparish.net/tutorial-tla/new-source.html
201 # http://regexps.srparish.net/tutorial-tla/importing-first.html
202 self._invoke_client('init-tree', self._project_name,
204 self._adjust_naming_conventions(path)
205 self._invoke_client('import', '--summary', 'Began versioning',
208 def _vcs_destroy(self):
209 if self._tmp_project == True:
210 self._remove_project()
211 if self._tmp_archive == True:
212 self._remove_archive()
213 vcs_dir = os.path.join(self.repo, '{arch}')
214 if os.path.exists(vcs_dir):
215 shutil.rmtree(vcs_dir)
216 self._archive_name = None
218 def _vcs_root(self, path):
219 if not os.path.isdir(path):
220 dirname = os.path.dirname(path)
223 status,output,error = self._u_invoke_client('tree-root', dirname)
224 root = output.rstrip('\n')
226 self._get_archive_project_name(root)
230 def _get_archive_name(self, root):
231 status,output,error = self._u_invoke_client('archives')
232 lines = output.split('\n')
234 # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52
235 # /tmp/BEtestXXXXXX/rootdir
237 for archive,location in zip(lines[::2], lines[1::2]):
238 if os.path.realpath(location) == os.path.realpath(root):
239 self._archive_name = archive
240 assert self._archive_name != None
242 def _get_archive_project_name(self, root):
244 status,output,error = self._u_invoke_client('tree-version', cwd=root)
246 # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1
247 archive_name,project_name = output.rstrip('\n').split('/')
248 self._archive_name = archive_name
249 self._project_name = project_name
251 def _vcs_get_user_id(self):
253 status,output,error = self._u_invoke_client('my-id')
254 return output.rstrip('\n')
256 if 'no arch user id set' in e.args[0]:
261 def _vcs_add(self, path):
262 self._u_invoke_client('add-id', path)
263 realpath = os.path.realpath(self._u_abspath(path))
264 pathAdded = realpath in self._list_added(self.repo)
265 if self.paranoid and not pathAdded:
266 self._force_source(path)
268 def _list_added(self, root):
269 assert os.path.exists(root)
270 assert os.access(root, os.X_OK)
271 root = os.path.realpath(root)
272 status,output,error = self._u_invoke_client('inventory', '--source',
273 '--both', '--all', root)
274 inv_str = output.rstrip('\n')
275 return [os.path.join(root, p) for p in inv_str.split('\n')]
277 def _add_dir_rule(self, rule, dirname, root):
278 inv_path = os.path.join(dirname, '.arch-inventory')
279 f = codecs.open(inv_path, 'a', self.encoding)
282 if os.path.realpath(inv_path) not in self._list_added(root):
283 paranoid = self.paranoid
284 self.paranoid = False
286 self.paranoid = paranoid
288 def _force_source(self, path):
289 rule = 'source %s\n' % self._u_rel_path(path)
290 self._add_dir_rule(rule, os.path.dirname(path), self.repo)
291 if os.path.realpath(path) not in self._list_added(self.repo):
292 raise CantAddFile(path)
294 def _vcs_remove(self, path):
295 if self._vcs_is_versioned(path):
296 self._u_invoke_client('delete-id', path)
297 arch_ids = os.path.join(self.repo, path, '.arch-ids')
298 if os.path.exists(arch_ids):
299 shutil.rmtree(arch_ids)
301 def _vcs_update(self, path):
302 self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618
304 def _vcs_is_versioned(self, path):
305 if '.arch-ids' in path:
309 def _vcs_get_file_contents(self, path, revision=None):
311 return base.VCS._vcs_get_file_contents(self, path, revision)
313 relpath = self._file_find(path, revision, relpath=True)
314 return base.VCS._vcs_get_file_contents(self, relpath)
316 def _file_find(self, path, revision, relpath=False):
318 status,output,error = \
320 'file-find', '--unescaped', path, revision)
321 path = output.rstrip('\n').splitlines()[-1]
322 except CommandError, e:
324 and 'illegally formed changeset index' in e.stderr:
325 raise NotImplementedError(
326 """Outstanding tla bug, see
327 https://bugs.launchpad.net/ubuntu/+source/tla/+bug/513472
332 return os.path.abspath(os.path.join(self.repo, path))
334 def _vcs_path(self, id, revision):
335 return self._u_find_id(id, revision)
337 def _vcs_isdir(self, path, revision):
338 abspath = self._file_find(path, revision)
339 return os.path.isdir(abspath)
341 def _vcs_listdir(self, path, revision):
342 abspath = self._file_find(path, revision)
343 return [p for p in os.listdir(abspath) if self._vcs_is_versioned(p)]
345 def _vcs_commit(self, commitfile, allow_empty=False):
346 if allow_empty == False:
347 # arch applies empty commits without complaining, so check first
348 status,output,error = self._u_invoke_client('changes',expect=(0,1))
350 # work around http://mercurial.selenic.com/bts/issue618
352 for path in self.__updated:
353 os.utime(os.path.join(self.repo, path), None)
355 status,output,error = self._u_invoke_client('changes',expect=(0,1))
358 raise base.EmptyCommit()
359 summary,body = self._u_parse_commitfile(commitfile)
360 args = ['commit', '--summary', summary]
362 args.extend(['--log-message',body])
363 status,output,error = self._u_invoke_client(*args)
365 revline = re.compile('[*] committed (.*)')
366 match = revline.search(output)
367 assert match != None, output+error
368 assert len(match.groups()) == 1
369 revpath = match.groups()[0]
370 assert not " " in revpath, revpath
371 assert revpath.startswith(self._archive_project_name()+'--')
372 revision = revpath[len(self._archive_project_name()+'--'):]
375 def _vcs_revision_id(self, index):
376 status,output,error = self._u_invoke_client('logs')
377 logs = output.splitlines()
378 first_log = logs.pop(0)
379 assert first_log == 'base-0', first_log
389 return '%s--%s' % (self._archive_project_name(), log)
391 def _diff(self, revision):
392 status,output,error = self._u_invoke_client(
393 'diff', '--summary', '--unescaped', revision, expect=(0,1))
396 def _parse_diff(self, diff_text):
400 * local directory is at ...
401 * build pristine tree for ...
402 * from import revision: ...
403 * patching for revision: ...
405 D .be/dir/bugs/.arch-ids/moved.id
406 D .be/dir/bugs/.arch-ids/removed.id
408 D .be/dir/bugs/removed
409 A .be/dir/bugs/.arch-ids/moved2.id
410 A .be/dir/bugs/.arch-ids/new.id
411 A .be/dir/bugs/moved2
413 A {arch}/bugs-everywhere/bugs-everywhere--mainline/...
414 M .be/dir/bugs/modified
419 lines = diff_text.splitlines()
420 for i,line in enumerate(lines):
421 if line.startswith('* ') or '/.arch-ids/' in line:
423 change,file = line.split(' ',1)
424 if file.startswith('{arch}/'):
429 modified.append(file)
432 return (new,modified,removed)
434 def _vcs_changed(self, revision):
435 return self._parse_diff(self._diff(revision))
438 if libbe.TESTING == True:
439 base.make_vcs_testcase_subclasses(Arch, sys.modules[__name__])
441 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
442 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])