1# Copyright (C) 2008 Canonical Ltd 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 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17"""View management. 18 19Views are contained within a working tree and normally constructed 20when first accessed. Clients should do, for example, ... 21 22 tree.views.lookup_view() 23""" 24 25import re 26 27from . import ( 28 errors, 29 osutils, 30 ) 31 32 33_VIEWS_FORMAT_MARKER_RE = re.compile(b'Bazaar views format (\\d+)') 34_VIEWS_FORMAT1_MARKER = b"Bazaar views format 1\n" 35 36 37class NoSuchView(errors.BzrError): 38 """A view does not exist. 39 """ 40 41 _fmt = u"No such view: %(view_name)s." 42 43 def __init__(self, view_name): 44 self.view_name = view_name 45 46 47class ViewsNotSupported(errors.BzrError): 48 """Views are not supported by a tree format. 49 """ 50 51 _fmt = ("Views are not supported by %(tree)s;" 52 " use 'brz upgrade' to change your tree to a later format.") 53 54 def __init__(self, tree): 55 self.tree = tree 56 57 58class FileOutsideView(errors.BzrError): 59 60 _fmt = ('Specified file "%(file_name)s" is outside the current view: ' 61 '%(view_str)s') 62 63 def __init__(self, file_name, view_files): 64 self.file_name = file_name 65 self.view_str = ", ".join(view_files) 66 67 68class _Views(object): 69 """Base class for View managers.""" 70 71 def supports_views(self): 72 raise NotImplementedError(self.supports_views) 73 74 75class PathBasedViews(_Views): 76 """View storage in an unversioned tree control file. 77 78 Views are stored in terms of paths relative to the tree root. 79 80 The top line of the control file is a format marker in the format: 81 82 Bazaar views format X 83 84 where X is an integer number. After this top line, version 1 format is 85 stored as follows: 86 87 * optional name-values pairs in the format 'name=value' 88 89 * optional view definitions, one per line in the format 90 91 views: 92 name file1 file2 ... 93 name file1 file2 ... 94 95 where the fields are separated by a nul character (\0). The views file 96 is encoded in utf-8. The only supported keyword in version 1 is 97 'current' which stores the name of the current view, if any. 98 """ 99 100 def __init__(self, tree): 101 self.tree = tree 102 self._loaded = False 103 self._current = None 104 self._views = {} 105 106 def supports_views(self): 107 return True 108 109 def get_view_info(self): 110 """Get the current view and dictionary of views. 111 112 :return: current, views where 113 current = the name of the current view or None if no view is enabled 114 views = a map from view name to list of files/directories 115 """ 116 self._load_view_info() 117 return self._current, self._views 118 119 def set_view_info(self, current, views): 120 """Set the current view and dictionary of views. 121 122 :param current: the name of the current view or None if no view is 123 enabled 124 :param views: a map from view name to list of files/directories 125 """ 126 if current is not None and current not in views: 127 raise NoSuchView(current) 128 with self.tree.lock_write(): 129 self._current = current 130 self._views = views 131 self._save_view_info() 132 133 def lookup_view(self, view_name=None): 134 """Return the contents of a view. 135 136 :param view_Name: name of the view or None to lookup the current view 137 :return: the list of files/directories in the requested view 138 """ 139 self._load_view_info() 140 try: 141 if view_name is None: 142 if self._current: 143 view_name = self._current 144 else: 145 return [] 146 return self._views[view_name] 147 except KeyError: 148 raise NoSuchView(view_name) 149 150 def set_view(self, view_name, view_files, make_current=True): 151 """Add or update a view definition. 152 153 :param view_name: the name of the view 154 :param view_files: the list of files/directories in the view 155 :param make_current: make this view the current one or not 156 """ 157 with self.tree.lock_write(): 158 self._load_view_info() 159 self._views[view_name] = view_files 160 if make_current: 161 self._current = view_name 162 self._save_view_info() 163 164 def delete_view(self, view_name): 165 """Delete a view definition. 166 167 If the view deleted is the current one, the current view is reset. 168 """ 169 with self.tree.lock_write(): 170 self._load_view_info() 171 try: 172 del self._views[view_name] 173 except KeyError: 174 raise NoSuchView(view_name) 175 if view_name == self._current: 176 self._current = None 177 self._save_view_info() 178 179 def _save_view_info(self): 180 """Save the current view and all view definitions. 181 182 Be sure to have initialised self._current and self._views before 183 calling this method. 184 """ 185 with self.tree.lock_write(): 186 if self._current is None: 187 keywords = {} 188 else: 189 keywords = {'current': self._current} 190 self.tree._transport.put_bytes( 191 'views', self._serialize_view_content(keywords, self._views)) 192 193 def _load_view_info(self): 194 """Load the current view and dictionary of view definitions.""" 195 if not self._loaded: 196 with self.tree.lock_read(): 197 try: 198 view_content = self.tree._transport.get_bytes('views') 199 except errors.NoSuchFile: 200 self._current, self._views = None, {} 201 else: 202 keywords, self._views = \ 203 self._deserialize_view_content(view_content) 204 self._current = keywords.get('current') 205 self._loaded = True 206 207 def _serialize_view_content(self, keywords, view_dict): 208 """Convert view keywords and a view dictionary into a stream.""" 209 lines = [_VIEWS_FORMAT1_MARKER] 210 for key in keywords: 211 line = "%s=%s\n" % (key, keywords[key]) 212 lines.append(line.encode('utf-8')) 213 if view_dict: 214 lines.append("views:\n".encode('utf-8')) 215 for view in sorted(view_dict): 216 view_data = "%s\0%s\n" % (view, "\0".join(view_dict[view])) 217 lines.append(view_data.encode('utf-8')) 218 return b"".join(lines) 219 220 def _deserialize_view_content(self, view_content): 221 """Convert a stream into view keywords and a dictionary of views.""" 222 # as a special case to make initialization easy, an empty definition 223 # maps to no current view and an empty view dictionary 224 if view_content == b'': 225 return {}, {} 226 lines = view_content.splitlines() 227 match = _VIEWS_FORMAT_MARKER_RE.match(lines[0]) 228 if not match: 229 raise ValueError( 230 "format marker missing from top of views file") 231 elif match.group(1) != b'1': 232 raise ValueError( 233 "cannot decode views format %s" % match.group(1)) 234 try: 235 keywords = {} 236 views = {} 237 in_views = False 238 for line in lines[1:]: 239 text = line.decode('utf-8') 240 if in_views: 241 parts = text.split('\0') 242 view = parts.pop(0) 243 views[view] = parts 244 elif text == 'views:': 245 in_views = True 246 continue 247 elif text.find('=') >= 0: 248 # must be a name-value pair 249 keyword, value = text.split('=', 1) 250 keywords[keyword] = value 251 else: 252 raise ValueError("failed to deserialize views line %s", 253 text) 254 return keywords, views 255 except ValueError as e: 256 raise ValueError("failed to deserialize views content %r: %s" 257 % (view_content, e)) 258 259 260class DisabledViews(_Views): 261 """View storage that refuses to store anything. 262 263 This is used by older formats that can't store views. 264 """ 265 266 def __init__(self, tree): 267 self.tree = tree 268 269 def supports_views(self): 270 return False 271 272 def _not_supported(self, *a, **k): 273 raise ViewsNotSupported(self.tree) 274 275 get_view_info = _not_supported 276 set_view_info = _not_supported 277 lookup_view = _not_supported 278 set_view = _not_supported 279 delete_view = _not_supported 280 281 282def view_display_str(view_files, encoding=None): 283 """Get the display string for a list of view files. 284 285 :param view_files: the list of file names 286 :param encoding: the encoding to display the files in 287 """ 288 if encoding is None: 289 return ", ".join(view_files) 290 else: 291 return ", ".join([v.encode(encoding, 'replace') for v in view_files]) 292 293 294def check_path_in_view(tree, relpath): 295 """If a working tree has a view enabled, check the path is within it.""" 296 if tree.supports_views(): 297 view_files = tree.views.lookup_view() 298 if view_files and not osutils.is_inside_any(view_files, relpath): 299 raise FileOutsideView(relpath, view_files) 300