1 # Copyright (C) 2010-2011 Chris Ball <cjb@laptop.org>
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 .. _Monotone: http://www.monotone.ca/
32 import libbe.ui.util.user
33 from libbe.util.subproc import CommandError
36 if libbe.TESTING == True:
44 class Monotone (base.VCS):
45 """:class:`base.VCS` implementation for Monotone.
50 def __init__(self, *args, **kwargs):
51 base.VCS.__init__(self, *args, **kwargs)
57 def _vcs_version(self):
58 status,output,error = self._u_invoke_client('automate', 'interface_version')
61 def version_cmp(self, *args):
62 """Compare the installed Monotone version `V_i` with another
63 version `V_o` (given in `*args`). Returns
74 >>> m = Monotone(repo='.')
75 >>> m._version = '7.1'
76 >>> m.version_cmp(7, 1)
78 >>> m.version_cmp(7, 2)
80 >>> m.version_cmp(7, 0)
82 >>> m.version_cmp(8, 0)
85 if not hasattr(self, '_parsed_version') \
86 or self._parsed_version == None:
87 self._parsed_version = [int(x) for x in self.version().split('.')]
88 for current,other in zip(self._parsed_version, args):
89 c = cmp(current,other)
94 def _require_version_ge(self, *args):
95 """Require installed interface version >= `*args`.
97 >>> m = Monotone(repo='.')
98 >>> m._version = '7.1'
99 >>> m._require_version_ge(6, 0)
100 >>> m._require_version_ge(7, 1)
101 >>> m._require_version_ge(7, 2)
102 Traceback (most recent call last):
104 NotImplementedError: Operation not supported for monotone automation interface version 7.1. Requires 7.2
106 if self.version_cmp(*args) < 0:
107 raise NotImplementedError(
108 'Operation not supported for %s automation interface version'
109 ' %s. Requires %s' % (self.name, self.version(),
110 '.'.join([str(x) for x in args])))
112 def _vcs_get_user_id(self):
113 status,output,error = self._u_invoke_client('list', 'keys')
117 # f7791378b49dfb47a740e9588848b510de58f64f john@doe.com
118 if '[private keys]' in output:
120 for line in output.splitlines():
122 if private == True: # HACK. Just pick the first key.
123 return line.split(' ', 1)[1]
124 if line == '[private keys]':
126 return None # Monotone has no infomation
128 def _vcs_detect(self, path):
129 if self._u_search_parent_directories(path, '_MTN') != None :
133 def _vcs_root(self, path):
134 """Find the root of the deepest repository containing path."""
135 if self.version_cmp(8, 0) >= 0:
136 if not os.path.isdir(path):
137 dirname = os.path.dirname(path)
140 status,output,error = self._invoke_client(
141 'automate', 'get_workspace_root', cwd=dirname)
143 mtn_dir = self._u_search_parent_directories(path, '_MTN')
146 return os.path.dirname(mtn_dir)
147 return output.strip()
149 def _invoke_client(self, *args, **kwargs):
150 """Invoke the client on our branch.
153 if self._db_path != None:
154 arglist.extend(['--db', self._db_path])
155 if self._key != None:
156 arglist.extend(['--key', self._key])
157 if self._key_dir != None:
158 arglist.extend(['--keydir', self._key_dir])
160 args = tuple(arglist)
161 return self._u_invoke_client(*args, **kwargs)
163 def _vcs_init(self, path):
164 self._require_version_ge(4, 0)
165 self._db_path = os.path.abspath(os.path.join(path, 'bugseverywhere.db'))
166 self._key_dir = os.path.abspath(os.path.join(path, '_monotone_keys'))
167 self._branch_name = 'bugs-everywhere-test'
168 self._key = 'bugseverywhere-%d@test.com' % random.randint(0,1e6)
169 self._passphrase = ''
170 self._u_invoke_client('db', 'init', '--db', self._db_path, cwd=path)
171 os.mkdir(self._key_dir)
172 self._u_invoke_client(
173 '--db', self._db_path,
174 '--keydir', self._key_dir,
175 'automate', 'genkey', self._key, self._passphrase)
177 'setup', '--db', self._db_path,
178 '--branch', self._branch_name, cwd=path)
180 def _vcs_destroy(self):
181 vcs_dir = os.path.join(self.repo, '_MTN')
182 for dir in [vcs_dir, self._key_dir]:
183 if os.path.exists(dir):
185 if os.path.exists(self._db_path):
186 os.remove(self._db_path)
188 def _vcs_add(self, path):
189 if os.path.isdir(path):
191 self._invoke_client('add', path)
193 def _vcs_remove(self, path):
194 if not os.path.isdir(self._u_abspath(path)):
195 self._invoke_client('rm', path)
197 def _vcs_update(self, path):
200 def _vcs_get_file_contents(self, path, revision=None):
202 return base.VCS._vcs_get_file_contents(self, path, revision)
204 self._require_version_ge(4, 0)
205 status,output,error = self._invoke_client(
206 'automate', 'get_file_of', path, '--revision', revision)
209 def _dirs_and_files(self, revision):
210 self._require_version_ge(2, 0)
211 status,output,error = self._invoke_client(
212 'automate', 'get_manifest_of', revision)
216 for line in output.splitlines():
217 fields = line.strip().split(' ', 1)
218 if len(fields) != 2 or len(fields[1]) < 2:
220 value = fields[1][1:-1] # [1:-1] for '"XYZ"' -> 'XYZ'
223 if fields[0] == 'dir':
225 children_by_dir[value] = []
226 elif fields[0] == 'file':
228 for child in (dirs+files):
233 # Does Monotone use native path separators?
234 start = p+os.path.sep
235 if p != child and child.startswith(start):
236 rel = child[len(start):]
237 if rel.count(os.path.sep) == 0:
240 children_by_dir[parent].append(child)
241 return (dirs, files, children_by_dir)
243 def _vcs_path(self, id, revision):
244 dirs,files,children_by_dir = self._dirs_and_files(revision)
245 return self._u_find_id_from_manifest(id, dirs+files, revision=revision)
247 def _vcs_isdir(self, path, revision):
248 dirs,files,children_by_dir = self._dirs_and_files(revision)
251 def _vcs_listdir(self, path, revision):
252 dirs,files,children_by_dir = self._dirs_and_files(revision)
253 children = [self._u_rel_path(c, path) for c in children_by_dir[path]]
256 def _vcs_commit(self, commitfile, allow_empty=False):
257 args = ['commit', '--key', self._key, '--message-file', commitfile]
258 kwargs = {'expect': (0,1)}
259 status,output,error = self._invoke_client(*args, **kwargs)
260 strings = ['no changes to commit']
261 current_rev = self._current_revision()
263 if self._u_any_in_string(strings, error) == True:
264 if allow_empty == False:
265 raise base.EmptyCommit()
266 # note that Monotone does _not_ make an empty revision.
267 # this returns the last non-empty revision id...
270 [self.client] + args, status, output, error)
271 else: # successful commit
272 assert current_rev in error, \
273 'Mismatched revisions:\n%s\n%s' % (current_rev, error)
276 def _current_revision(self):
277 self._require_version_ge(2, 0)
278 status,output,error = self._invoke_client(
279 'automate', 'get_base_revision_id') # since 2.0
280 return output.strip()
282 def _vcs_revision_id(self, index):
283 current_rev = self._current_revision()
284 status,output,error = self._invoke_client(
285 'automate', 'ancestors', current_rev) # since 0.2, but output is alphebetized
286 revs = output.splitlines() + [current_rev]
287 status,output,error = self._invoke_client(
288 'automate', 'toposort', *revs)
289 revisions = output.splitlines()
292 return revisions[index-1]
294 return revisions[index]
300 def _diff(self, revision):
301 status,output,error = self._invoke_client('-r', revision, 'diff')
304 def _parse_diff(self, diff_text):
305 """_parse_diff(diff_text) -> (new,modified,removed)
307 `new`, `modified`, and `removed` are lists of files.
312 # old_revision [1ce9ac2cfe3166b8ad23a60555f8a70f37686c25]
314 # delete ".be/dir/bugs/moved"
316 # delete ".be/dir/bugs/removed"
318 # add_file ".be/dir/bugs/moved2"
319 # content [33e4510df9abef16dad7c65c0775e74602cc5005]
321 # add_file ".be/dir/bugs/new"
322 # content [45c45b5630f7446f83b0e14ee1525e449a06131c]
324 # patch ".be/dir/bugs/modified"
325 # from [809bf3b80423c361849386008a0ce01199d30929]
326 # to [f13d3ec08972e2b41afecd9a90d4bc71cdcea338]
328 ============================================================
329 --- .be/dir/bugs/moved2 33e4510df9abef16dad7c65c0775e74602cc5005
330 +++ .be/dir/bugs/moved2 33e4510df9abef16dad7c65c0775e74602cc5005
332 +this entry will be moved
333 \ No newline at end of file
334 ============================================================
335 --- .be/dir/bugs/new 45c45b5630f7446f83b0e14ee1525e449a06131c
336 +++ .be/dir/bugs/new 45c45b5630f7446f83b0e14ee1525e449a06131c
339 \ No newline at end of file
340 ============================================================
341 --- .be/dir/bugs/modified 809bf3b80423c361849386008a0ce01199d30929
342 +++ .be/dir/bugs/modified f13d3ec08972e2b41afecd9a90d4bc71cdcea338
344 -some value to be modified
345 \ No newline at end of file
347 \ No newline at end of file
352 lines = diff_text.splitlines()
353 for i,line in enumerate(lines):
354 if line.startswith('# add_file "'):
355 new.append(line[len('# add_file "'):-1])
356 elif line.startswith('# patch "'):
357 modified.append(line[len('# patch "'):-1])
358 elif line.startswith('# delete "'):
359 removed.append(line[len('# delete "'):-1])
360 elif not line.startswith('#'):
362 return (new,modified,removed)
364 def _vcs_changed(self, revision):
365 return self._parse_diff(self._diff(revision))
368 if libbe.TESTING == True:
369 base.make_vcs_testcase_subclasses(Monotone, sys.modules[__name__])
371 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
372 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])