1862975ef801ce3826a30c7a9ded8aee61d158c0
[be.git] / libbe / storage / vcs / bzr.py
1 # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
2 #                         Ben Finney <benf@cybersource.com.au>
3 #                         Gianluca Montecchi <gian@grys.it>
4 #                         Marien Zwart <marien.zwart@gmail.com>
5 #                         W. Trevor King <wking@drexel.edu>
6 #
7 # This file is part of Bugs Everywhere.
8 #
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.
13 #
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.
18 #
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/>.
21
22 """Bazaar_ (bzr) backend.
23
24 .. _Bazaar: http://bazaar.canonical.com/
25 """
26
27 try:
28     import bzrlib
29     import bzrlib.branch
30     import bzrlib.builtins
31     import bzrlib.config
32     import bzrlib.errors
33     import bzrlib.option
34 except ImportError:
35     bzrlib = None
36 import os
37 import os.path
38 import re
39 import shutil
40 import StringIO
41 import sys
42 import types
43
44 import libbe
45 import base
46
47 if libbe.TESTING == True:
48     import doctest
49     import unittest
50
51
52 def new():
53     return Bzr()
54
55 class Bzr(base.VCS):
56     """:class:`base.VCS` implementation for Bazaar.
57     """
58     name = 'bzr'
59     client = None # bzrlib module
60
61     def __init__(self, *args, **kwargs):
62         base.VCS.__init__(self, *args, **kwargs)
63         self.versioned = True
64
65     def _vcs_version(self):
66         if bzrlib == None:
67             return None
68         return bzrlib.__version__
69
70     def version_cmp(self, *args):
71         """Compare the installed Bazaar version `V_i` with another version
72         `V_o` (given in `*args`).  Returns
73
74            === ===============
75             1  if `V_i > V_o`
76             0  if `V_i == V_o`
77            -1  if `V_i < V_o`
78            === ===============
79
80         Examples
81         --------
82
83         >>> b = Bzr(repo='.')
84         >>> b._version = '2.3.1 (release)'
85         >>> b.version_cmp(2,3,1)
86         0
87         >>> b.version_cmp(2,3,2)
88         -1
89         >>> b.version_cmp(2,3,0)
90         1
91         >>> b.version_cmp(3)
92         -1
93         >>> b._version = '2.0.0pre2'
94         >>> b._parsed_version = None
95         >>> b.version_cmp(3)
96         -1
97         >>> b.version_cmp(2,0,1)
98         Traceback (most recent call last):
99           ...
100         NotImplementedError: Cannot parse non-integer portion "0pre2" of Bzr version "2.0.0pre2"
101         """
102         if not hasattr(self, '_parsed_version') \
103                 or self._parsed_version == None:
104             num_part = self.version().split(' ')[0]
105             self._parsed_version = []
106             for num in num_part.split('.'):
107                 try:
108                     self._parsed_version.append(int(num))
109                 except ValueError, e:
110                     self._parsed_version.append(num)
111         for current,other in zip(self._parsed_version, args):
112             if type(current) != types.IntType:
113                 raise NotImplementedError(
114                     'Cannot parse non-integer portion "%s" of Bzr version "%s"'
115                     % (current, self.version()))
116             c = cmp(current,other)
117             if c != 0:
118                 return c
119         return 0
120
121     def _vcs_get_user_id(self):
122         # excerpted from bzrlib.builtins.cmd_whoami.run()
123         try:
124             c = bzrlib.branch.Branch.open_containing(self.repo)[0].get_config()
125         except errors.NotBranchError:
126             c = bzrlib.config.GlobalConfig()
127         return c.username()
128
129     def _vcs_detect(self, path):
130         if self._u_search_parent_directories(path, '.bzr') != None :
131             return True
132         return False
133
134     def _vcs_root(self, path):
135         """Find the root of the deepest repository containing path."""
136         cmd = bzrlib.builtins.cmd_root()
137         cmd.outf = StringIO.StringIO()
138         cmd.run(filename=path)
139         cmd.cleanup_now()
140         return cmd.outf.getvalue().rstrip('\n')
141
142     def _vcs_init(self, path):
143         cmd = bzrlib.builtins.cmd_init()
144         cmd.outf = StringIO.StringIO()
145         cmd.run(location=path)
146         cmd.cleanup_now()
147
148     def _vcs_destroy(self):
149         vcs_dir = os.path.join(self.repo, '.bzr')
150         if os.path.exists(vcs_dir):
151             shutil.rmtree(vcs_dir)
152
153     def _vcs_add(self, path):
154         path = os.path.join(self.repo, path)
155         cmd = bzrlib.builtins.cmd_add()
156         cmd.outf = StringIO.StringIO()
157         cmd.run(file_list=[path], file_ids_from=self.repo)
158         cmd.cleanup_now()
159
160     def _vcs_exists(self, path, revision=None):
161         manifest = self._vcs_listdir(
162             self.repo, revision=revision, recursive=True)
163         if path in manifest:
164             return True
165         return False
166
167     def _vcs_remove(self, path):
168         # --force to also remove unversioned files.
169         path = os.path.join(self.repo, path)
170         cmd = bzrlib.builtins.cmd_remove()
171         cmd.outf = StringIO.StringIO()
172         cmd.run(file_list=[path], file_deletion_strategy='force')
173         cmd.cleanup_now()
174
175     def _vcs_update(self, path):
176         pass
177
178     def _parse_revision_string(self, revision=None):
179         if revision == None:
180             return revision
181         rev_opt = bzrlib.option.Option.OPTIONS['revision']
182         try:
183             rev_spec = rev_opt.type(revision)
184         except bzrlib.errors.NoSuchRevisionSpec:
185             raise base.InvalidRevision(revision)
186         return rev_spec
187
188     def _vcs_get_file_contents(self, path, revision=None):
189         if revision == None:
190             return base.VCS._vcs_get_file_contents(self, path, revision)
191         path = os.path.join(self.repo, path)
192         revision = self._parse_revision_string(revision)
193         cmd = bzrlib.builtins.cmd_cat()
194         cmd.outf = StringIO.StringIO()
195         if self.version_cmp(1,6,0) < 0:
196             # old bzrlib cmd_cat uses sys.stdout not self.outf for output.
197             stdout = sys.stdout
198             sys.stdout = cmd.outf
199         try:
200             cmd.run(filename=path, revision=revision)
201         except bzrlib.errors.BzrCommandError, e:
202             if 'not present in revision' in str(e):
203                 raise base.InvalidPath(path, root=self.repo, revision=revision)
204             raise
205         finally:
206             if self.version_cmp(2,0,0) < 0:
207                 cmd.outf = sys.stdout
208                 sys.stdout = stdout
209             cmd.cleanup_now()
210         return cmd.outf.getvalue()
211
212     def _vcs_path(self, id, revision):
213         manifest = self._vcs_listdir(
214             self.repo, revision=revision, recursive=True)
215         return self._u_find_id_from_manifest(id, manifest, revision=revision)
216
217     def _vcs_isdir(self, path, revision):
218         try:
219             self._vcs_listdir(path, revision)
220         except AttributeError, e:
221             if 'children' in str(e):
222                 return False
223             raise
224         return True
225
226     def _vcs_listdir(self, path, revision, recursive=False):
227         path = os.path.join(self.repo, path)
228         revision = self._parse_revision_string(revision)
229         cmd = bzrlib.builtins.cmd_ls()
230         cmd.outf = StringIO.StringIO()
231         try:
232             if self.version_cmp(2,0,0) >= 0:
233                 cmd.run(revision=revision, path=path, recursive=recursive)
234             else:
235                 # Pre-2.0 Bazaar (non_recursive)
236                 # + working around broken non_recursive+path implementation
237                 #   (https://bugs.launchpad.net/bzr/+bug/158690)
238                 cmd.run(revision=revision, path=path,
239                         non_recursive=False)
240         except bzrlib.errors.BzrCommandError, e:
241             if 'not present in revision' in str(e):
242                 raise base.InvalidPath(path, root=self.repo, revision=revision)
243             raise
244         finally:
245             cmd.cleanup_now()
246         children = cmd.outf.getvalue().rstrip('\n').splitlines()
247         children = [self._u_rel_path(c, path) for c in children]
248         if self.version_cmp(2,0,0) < 0 and recursive == False:
249             children = [c for c in children if os.path.sep not in c]
250         return children
251
252     def _vcs_commit(self, commitfile, allow_empty=False):
253         cmd = bzrlib.builtins.cmd_commit()
254         cmd.outf = StringIO.StringIO()
255         cwd = os.getcwd()
256         os.chdir(self.repo)
257         try:
258             cmd.run(file=commitfile, unchanged=allow_empty)
259         except bzrlib.errors.BzrCommandError, e:
260             strings = ['no changes to commit.', # bzr 1.3.1
261                        'No changes to commit.'] # bzr 1.15.1
262             if self._u_any_in_string(strings, str(e)) == True:
263                 raise base.EmptyCommit()
264             raise
265         finally:
266             os.chdir(cwd)
267             cmd.cleanup_now()
268         return self._vcs_revision_id(-1)
269
270     def _vcs_revision_id(self, index):
271         cmd = bzrlib.builtins.cmd_revno()
272         cmd.outf = StringIO.StringIO()
273         cmd.run(location=self.repo)
274         cmd.cleanup_now()
275         current_revision = int(cmd.outf.getvalue())
276         if index > current_revision or index < -current_revision:
277             return None
278         if index >= 0:
279             return str(index) # bzr commit 0 is the empty tree.
280         return str(current_revision+index+1)
281
282     def _diff(self, revision):
283         revision = self._parse_revision_string(revision)
284         cmd = bzrlib.builtins.cmd_diff()
285         cmd.outf = StringIO.StringIO()
286         # for some reason, cmd_diff uses sys.stdout not self.outf for output.
287         stdout = sys.stdout
288         sys.stdout = cmd.outf
289         try:
290             status = cmd.run(revision=revision, file_list=[self.repo])
291         finally:
292             sys.stdout = stdout
293             cmd.cleanup_now()
294         assert status in [0,1], "Invalid status %d" % status
295         return cmd.outf.getvalue()
296
297     def _parse_diff(self, diff_text):
298         """_parse_diff(diff_text) -> (new,modified,removed)
299
300         `new`, `modified`, and `removed` are lists of files.
301
302         Example diff text::
303
304           === modified file 'dir/changed'
305           --- dir/changed       2010-01-16 01:54:53 +0000
306           +++ dir/changed       2010-01-16 01:54:54 +0000
307           @@ -1,3 +1,3 @@
308            hi
309           -there
310           +everyone and
311            joe
312           
313           === removed file 'dir/deleted'
314           --- dir/deleted       2010-01-16 01:54:53 +0000
315           +++ dir/deleted       1970-01-01 00:00:00 +0000
316           @@ -1,3 +0,0 @@
317           -in
318           -the
319           -beginning
320           
321           === removed file 'dir/moved'
322           --- dir/moved 2010-01-16 01:54:53 +0000
323           +++ dir/moved 1970-01-01 00:00:00 +0000
324           @@ -1,4 +0,0 @@
325           -the
326           -ants
327           -go
328           -marching
329           
330           === added file 'dir/moved2'
331           --- dir/moved2        1970-01-01 00:00:00 +0000
332           +++ dir/moved2        2010-01-16 01:54:34 +0000
333           @@ -0,0 +1,4 @@
334           +the
335           +ants
336           +go
337           +marching
338           
339           === added file 'dir/new'
340           --- dir/new   1970-01-01 00:00:00 +0000
341           +++ dir/new   2010-01-16 01:54:54 +0000
342           @@ -0,0 +1,2 @@
343           +hello
344           +world
345           
346         """
347         new = []
348         modified = []
349         removed = []
350         for line in diff_text.splitlines():
351             if not line.startswith('=== '):
352                 continue
353             fields = line.split()
354             action = fields[1]
355             file = fields[-1].strip("'")
356             if action == 'added':
357                 new.append(file)
358             elif action == 'modified':
359                 modified.append(file)
360             elif action == 'removed':
361                 removed.append(file)
362         return (new,modified,removed)
363
364     def _vcs_changed(self, revision):
365         return self._parse_diff(self._diff(revision))
366
367 \f
368 if libbe.TESTING == True:
369     base.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__])
370
371     unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
372     suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])