Merged Anton Batenev's report of Nicolas Alvarez' unicode-in-be-new bug
[be.git] / libbe / storage / util / upgrade.py
1 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17 """
18 Handle conversion between the various BE storage formats.
19 """
20
21 import codecs
22 import os, os.path
23 import sys
24
25 import libbe
26 import libbe.bug
27 import libbe.storage.util.mapfile as mapfile
28 from libbe.storage import STORAGE_VERSIONS, STORAGE_VERSION
29 #import libbe.storage.vcs # delay import to avoid cyclic dependency
30 import libbe.ui.util.editor
31 import libbe.util
32 import libbe.util.encoding as encoding
33 import libbe.util.id
34
35
36 class Upgrader (object):
37     "Class for converting between different on-disk BE storage formats."
38     initial_version = None
39     final_version = None
40     def __init__(self, repo):
41         import libbe.storage.vcs
42
43         self.repo = repo
44         vcs_name = self._get_vcs_name()
45         if vcs_name == None:
46             vcs_name = 'None'
47         self.vcs = libbe.storage.vcs.vcs_by_name(vcs_name)
48         self.vcs.repo = self.repo
49         self.vcs.root()
50
51     def get_path(self, *args):
52         """
53         Return the absolute path using args relative to .be.
54         """
55         dir = os.path.join(self.repo, '.be')
56         if len(args) == 0:
57             return dir
58         return os.path.join(dir, *args)
59
60     def _get_vcs_name(self):
61         return None
62
63     def check_initial_version(self):
64         path = self.get_path('version')
65         version = encoding.get_file_contents(path, decode=True).rstrip('\n')
66         assert version == self.initial_version, '%s: %s' % (path, version)
67
68     def set_version(self):
69         path = self.get_path('version')
70         encoding.set_file_contents(path, self.final_version+'\n')
71         self.vcs._vcs_update(path)
72
73     def upgrade(self):
74         print >> sys.stderr, 'upgrading bugdir from "%s" to "%s"' \
75             % (self.initial_version, self.final_version)
76         self.check_initial_version()
77         self.set_version()
78         self._upgrade()
79
80     def _upgrade(self):
81         raise NotImplementedError
82
83
84 class Upgrade_1_0_to_1_1 (Upgrader):
85     initial_version = "Bugs Everywhere Tree 1 0"
86     final_version = "Bugs Everywhere Directory v1.1"
87     def _get_vcs_name(self):
88         path = self.get_path('settings')
89         settings = encoding.get_file_contents(path)
90         for line in settings.splitlines(False):
91             fields = line.split('=')
92             if len(fields) == 2 and fields[0] == 'rcs_name':
93                 return fields[1]
94         return None
95             
96     def _upgrade_mapfile(self, path):
97         contents = encoding.get_file_contents(path, decode=True)
98         old_format = False
99         for line in contents.splitlines():
100             if len(line.split('=')) == 2:
101                 old_format = True
102                 break
103         if old_format == True:
104             # translate to YAML.
105             newlines = []
106             for line in contents.splitlines():
107                 line = line.rstrip('\n')
108                 if len(line) == 0:
109                     continue
110                 fields = line.split("=")
111                 if len(fields) == 2:
112                     key,value = fields
113                     newlines.append('%s: "%s"' % (key, value.replace('"','\\"')))
114                 else:
115                     newlines.append(line)
116             contents = '\n'.join(newlines)
117             # load the YAML and save
118             map = mapfile.parse(contents)
119             contents = mapfile.generate(map)
120             encoding.set_file_contents(path, contents)
121             self.vcs._vcs_update(path)
122
123     def _upgrade(self):
124         """
125         Comment value field "From" -> "Author".
126         Homegrown mapfile -> YAML.
127         """
128         path = self.get_path('settings')
129         self._upgrade_mapfile(path)
130         for bug_uuid in os.listdir(self.get_path('bugs')):
131             path = self.get_path('bugs', bug_uuid, 'values')
132             self._upgrade_mapfile(path)
133             c_path = ['bugs', bug_uuid, 'comments']
134             if not os.path.exists(self.get_path(*c_path)):
135                 continue # no comments for this bug
136             for comment_uuid in os.listdir(self.get_path(*c_path)):
137                 path_list = c_path + [comment_uuid, 'values']
138                 path = self.get_path(*path_list)
139                 self._upgrade_mapfile(path)
140                 settings = mapfile.parse(
141                     encoding.get_file_contents(path))
142                 if 'From' in settings:
143                     settings['Author'] = settings.pop('From')
144                     encoding.set_file_contents(
145                         path, mapfile.generate(settings))
146                     self.vcs._vcs_update(path)
147
148
149 class Upgrade_1_1_to_1_2 (Upgrader):
150     initial_version = "Bugs Everywhere Directory v1.1"
151     final_version = "Bugs Everywhere Directory v1.2"
152     def _get_vcs_name(self):
153         path = self.get_path('settings')
154         settings = mapfile.parse(encoding.get_file_contents(path))
155         if 'rcs_name' in settings:
156             return settings['rcs_name']
157         return None
158             
159     def _upgrade(self):
160         """
161         BugDir settings field "rcs_name" -> "vcs_name".
162         """
163         path = self.get_path('settings')
164         settings = mapfile.parse(encoding.get_file_contents(path))
165         if 'rcs_name' in settings:
166             settings['vcs_name'] = settings.pop('rcs_name')
167             encoding.set_file_contents(path, mapfile.generate(settings))
168             self.vcs._vcs_update(path)
169
170 class Upgrade_1_2_to_1_3 (Upgrader):
171     initial_version = "Bugs Everywhere Directory v1.2"
172     final_version = "Bugs Everywhere Directory v1.3"
173     def __init__(self, *args, **kwargs):
174         Upgrader.__init__(self, *args, **kwargs)
175         self._targets = {} # key: target text,value: new target bug
176
177     def _get_vcs_name(self):
178         path = self.get_path('settings')
179         settings = mapfile.parse(encoding.get_file_contents(path))
180         if 'vcs_name' in settings:
181             return settings['vcs_name']
182         return None
183
184     def _save_bug_settings(self, bug):
185         # The target bugs don't have comments
186         path = self.get_path('bugs', bug.uuid, 'values')
187         if not os.path.exists(path):
188             self.vcs._add_path(path, directory=False)
189         path = self.get_path('bugs', bug.uuid, 'values')
190         mf = mapfile.generate(bug._get_saved_settings())
191         encoding.set_file_contents(path, mf)
192         self.vcs._vcs_update(path)
193
194     def _target_bug(self, target_text):
195         if target_text not in self._targets:
196             bug = libbe.bug.Bug(summary=target_text)
197             bug.severity = 'target'
198             self._targets[target_text] = bug
199         return self._targets[target_text]
200
201     def _upgrade_bugdir_mapfile(self):
202         path = self.get_path('settings')
203         mf = encoding.get_file_contents(path)
204         if mf == libbe.util.InvalidObject:
205             return # settings file does not exist
206         settings = mapfile.parse(mf)
207         if 'target' in settings:
208             settings['target'] = self._target_bug(settings['target']).uuid
209             mf = mapfile.generate(settings)
210             encoding.set_file_contents(path, mf)
211             self.vcs._vcs_update(path)
212
213     def _upgrade_bug_mapfile(self, bug_uuid):
214         import libbe.command.depend as dep
215         path = self.get_path('bugs', bug_uuid, 'values')
216         mf = encoding.get_file_contents(path)
217         if mf == libbe.util.InvalidObject:
218             return # settings file does not exist
219         settings = mapfile.parse(mf)
220         if 'target' in settings:
221             target_bug = self._target_bug(settings['target'])
222
223             blocked_by_string = '%s%s' % (dep.BLOCKED_BY_TAG, bug_uuid)
224             dep._add_remove_extra_string(target_bug, blocked_by_string, add=True)
225             blocks_string = dep._generate_blocks_string(target_bug)
226             estrs = settings.get('extra_strings', [])
227             estrs.append(blocks_string)
228             settings['extra_strings'] = sorted(estrs)
229
230             settings.pop('target')
231             mf = mapfile.generate(settings)
232             encoding.set_file_contents(path, mf)
233             self.vcs._vcs_update(path)
234
235     def _upgrade(self):
236         """
237         Bug value field "target" -> target bugs.
238         Bugdir value field "target" -> pointer to current target bug.
239         """
240         for bug_uuid in os.listdir(self.get_path('bugs')):
241             self._upgrade_bug_mapfile(bug_uuid)
242         self._upgrade_bugdir_mapfile()
243         for bug in self._targets.values():
244             self._save_bug_settings(bug)
245
246 class Upgrade_1_3_to_1_4 (Upgrader):
247     initial_version = "Bugs Everywhere Directory v1.3"
248     final_version = "Bugs Everywhere Directory v1.4"
249     def _get_vcs_name(self):
250         path = self.get_path('settings')
251         settings = mapfile.parse(encoding.get_file_contents(path))
252         if 'vcs_name' in settings:
253             return settings['vcs_name']
254         return None
255
256     def _upgrade(self):
257         """
258         add new directory "./be/BUGDIR-UUID"
259         "./be/bugs" -> "./be/BUGDIR-UUID/bugs"
260         "./be/settings" -> "./be/BUGDIR-UUID/settings"
261         """
262         self.repo = os.path.abspath(self.repo)
263         basenames = [p for p in os.listdir(self.get_path())]
264         if not 'bugs' in basenames and not 'settings' in basenames \
265                 and len([p for p in basenames if len(p)==36]) == 1:
266             return # the user has upgraded the directory.
267         basenames = [p for p in basenames if p in ['bugs','settings']]
268         uuid = libbe.util.id.uuid_gen()
269         add = [self.get_path(uuid)]
270         move = [(self.get_path(p), self.get_path(uuid, p)) for p in basenames]
271         msg = ['Upgrading BE directory version v1.3 to v1.4',
272                '',
273                "Because BE's VCS drivers don't support 'move',",
274                'please make the following changes with your VCS',
275                'and re-run BE.  Note that you can choose a different',
276                'bugdir UUID to preserve uniformity across branches',
277                'of a distributed repository.'
278                '',
279                'add',
280                '  ' + '\n  '.join(add),
281                'move',
282                '  ' + '\n  '.join(['%s %s' % (a,b) for a,b in move]),
283                ]
284         self.vcs._cached_path_id.destroy()
285         raise Exception('Need user assistance\n%s' % '\n'.join(msg))
286
287
288 upgraders = [Upgrade_1_0_to_1_1,
289              Upgrade_1_1_to_1_2,
290              Upgrade_1_2_to_1_3,
291              Upgrade_1_3_to_1_4]
292 upgrade_classes = {}
293 for upgrader in upgraders:
294     upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader
295
296 def upgrade(path, current_version,
297             target_version=STORAGE_VERSION):
298     """
299     Call the appropriate upgrade function to convert current_version
300     to target_version.  If a direct conversion function does not exist,
301     use consecutive conversion functions.
302     """
303     if current_version not in STORAGE_VERSIONS:
304         raise NotImplementedError, \
305             "Cannot handle version '%s' yet." % current_version
306     if target_version not in STORAGE_VERSIONS:
307         raise NotImplementedError, \
308             "Cannot handle version '%s' yet." % current_version
309
310     if (current_version, target_version) in upgrade_classes:
311         # direct conversion
312         upgrade_class = upgrade_classes[(current_version, target_version)]
313         u = upgrade_class(path)
314         u.upgrade()
315     else:
316         # consecutive single-step conversion
317         i = STORAGE_VERSIONS.index(current_version)
318         while True:
319             version_a = STORAGE_VERSIONS[i]
320             version_b = STORAGE_VERSIONS[i+1]
321             try:
322                 upgrade_class = upgrade_classes[(version_a, version_b)]
323             except KeyError:
324                 raise NotImplementedError, \
325                     "Cannot convert version '%s' to '%s' yet." \
326                     % (version_a, version_b)
327             u = upgrade_class(path)
328             u.upgrade()
329             if version_b == target_version:
330                 break
331             i += 1