1
 
# Copyright (C) 2008 Canonical Ltd
 
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.
 
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.
 
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
 
19
 
Views are contained within a working tree and normally constructed
 
20
 
when first accessed.  Clients should do, for example, ...
 
22
 
  tree.views.lookup_view()
 
34
 
_VIEWS_FORMAT_MARKER_RE = re.compile(r'Bazaar views format (\d+)')
 
35
 
_VIEWS_FORMAT1_MARKER = "Bazaar views format 1\n"
 
39
 
    """Base class for View managers."""
 
41
 
    def supports_views(self):
 
42
 
        raise NotImplementedError(self.supports_views)
 
45
 
class PathBasedViews(_Views):
 
46
 
    """View storage in an unversioned tree control file.
 
48
 
    Views are stored in terms of paths relative to the tree root.
 
50
 
    The top line of the control file is a format marker in the format:
 
54
 
    where X is an integer number. After this top line, version 1 format is
 
57
 
     * optional name-values pairs in the format 'name=value'
 
59
 
     * optional view definitions, one per line in the format
 
65
 
    where the fields are separated by a nul character (\0). The views file
 
66
 
    is encoded in utf-8. The only supported keyword in version 1 is
 
67
 
    'current' which stores the name of the current view, if any.
 
70
 
    def __init__(self, tree):
 
76
 
    def supports_views(self):
 
79
 
    def get_view_info(self):
 
80
 
        """Get the current view and dictionary of views.
 
82
 
        :return: current, views where
 
83
 
          current = the name of the current view or None if no view is enabled
 
84
 
          views = a map from view name to list of files/directories
 
86
 
        self._load_view_info()
 
87
 
        return self._current, self._views
 
89
 
    def set_view_info(self, current, views):
 
90
 
        """Set the current view and dictionary of views.
 
92
 
        :param current: the name of the current view or None if no view is
 
94
 
        :param views: a map from view name to list of files/directories
 
96
 
        if current is not None and current not in views:
 
97
 
            raise errors.NoSuchView(current)
 
98
 
        self.tree.lock_write()
 
100
 
            self._current = current
 
102
 
            self._save_view_info()
 
106
 
    def lookup_view(self, view_name=None):
 
107
 
        """Return the contents of a view.
 
109
 
        :param view_Name: name of the view or None to lookup the current view
 
110
 
        :return: the list of files/directories in the requested view
 
112
 
        self._load_view_info()
 
114
 
            if view_name is None:
 
116
 
                    view_name = self._current
 
119
 
            return self._views[view_name]
 
121
 
            raise errors.NoSuchView(view_name)
 
123
 
    def set_view(self, view_name, view_files, make_current=True):
 
124
 
        """Add or update a view definition.
 
126
 
        :param view_name: the name of the view
 
127
 
        :param view_files: the list of files/directories in the view
 
128
 
        :param make_current: make this view the current one or not
 
130
 
        self.tree.lock_write()
 
132
 
            self._load_view_info()
 
133
 
            self._views[view_name] = view_files
 
135
 
                self._current = view_name
 
136
 
            self._save_view_info()
 
140
 
    def delete_view(self, view_name):
 
141
 
        """Delete a view definition.
 
143
 
        If the view deleted is the current one, the current view is reset.
 
145
 
        self.tree.lock_write()
 
147
 
            self._load_view_info()
 
149
 
                del self._views[view_name]
 
151
 
                raise errors.NoSuchView(view_name)
 
152
 
            if view_name == self._current:
 
154
 
            self._save_view_info()
 
158
 
    def _save_view_info(self):
 
159
 
        """Save the current view and all view definitions.
 
161
 
        Be sure to have initialised self._current and self._views before
 
164
 
        self.tree.lock_write()
 
166
 
            if self._current is None:
 
169
 
                keywords = {'current': self._current}
 
170
 
            self.tree._transport.put_bytes('views',
 
171
 
                self._serialize_view_content(keywords, self._views))
 
175
 
    def _load_view_info(self):
 
176
 
        """Load the current view and dictionary of view definitions."""
 
178
 
            self.tree.lock_read()
 
181
 
                    view_content = self.tree._transport.get_bytes('views')
 
182
 
                except errors.NoSuchFile, e:
 
183
 
                    self._current, self._views = None, {}
 
185
 
                    keywords, self._views = \
 
186
 
                        self._deserialize_view_content(view_content)
 
187
 
                    self._current = keywords.get('current')
 
192
 
    def _serialize_view_content(self, keywords, view_dict):
 
193
 
        """Convert view keywords and a view dictionary into a stream."""
 
194
 
        lines = [_VIEWS_FORMAT1_MARKER]
 
196
 
            line = "%s=%s\n" % (key,keywords[key])
 
197
 
            lines.append(line.encode('utf-8'))
 
199
 
            lines.append("views:\n".encode('utf-8'))
 
200
 
            for view in sorted(view_dict):
 
201
 
                view_data = "%s\0%s\n" % (view, "\0".join(view_dict[view]))
 
202
 
                lines.append(view_data.encode('utf-8'))
 
203
 
        return "".join(lines)
 
205
 
    def _deserialize_view_content(self, view_content):
 
206
 
        """Convert a stream into view keywords and a dictionary of views."""
 
207
 
        # as a special case to make initialization easy, an empty definition
 
208
 
        # maps to no current view and an empty view dictionary
 
209
 
        if view_content == '':
 
211
 
        lines = view_content.splitlines()
 
212
 
        match = _VIEWS_FORMAT_MARKER_RE.match(lines[0])
 
215
 
                "format marker missing from top of views file")
 
216
 
        elif match.group(1) != '1':
 
218
 
                "cannot decode views format %s" % match.group(1))
 
223
 
            for line in lines[1:]:
 
224
 
                text = line.decode('utf-8')
 
226
 
                    parts = text.split('\0')
 
229
 
                elif text == 'views:':
 
232
 
                elif text.find('=') >= 0:
 
233
 
                    # must be a name-value pair
 
234
 
                    keyword, value = text.split('=', 1)
 
235
 
                    keywords[keyword] = value
 
237
 
                    raise ValueError("failed to deserialize views line %s",
 
239
 
            return keywords, views
 
240
 
        except ValueError, e:
 
241
 
            raise ValueError("failed to deserialize views content %r: %s"
 
245
 
class DisabledViews(_Views):
 
246
 
    """View storage that refuses to store anything.
 
248
 
    This is used by older formats that can't store views.
 
251
 
    def __init__(self, tree):
 
254
 
    def supports_views(self):
 
257
 
    def _not_supported(self, *a, **k):
 
258
 
        raise errors.ViewsNotSupported(self.tree)
 
260
 
    get_view_info = _not_supported
 
261
 
    set_view_info = _not_supported
 
262
 
    lookup_view = _not_supported
 
263
 
    set_view = _not_supported
 
264
 
    delete_view = _not_supported
 
267
 
def view_display_str(view_files, encoding=None):
 
268
 
    """Get the display string for a list of view files.
 
270
 
    :param view_files: the list of file names
 
271
 
    :param encoding: the encoding to display the files in
 
274
 
        return ", ".join(view_files)
 
276
 
        return ", ".join([v.encode(encoding, 'replace') for v in view_files])
 
279
 
def check_path_in_view(tree, relpath):
 
280
 
    """If a working tree has a view enabled, check the path is within it."""
 
281
 
    if tree.supports_views():
 
282
 
        view_files = tree.views.lookup_view()
 
283
 
        if  view_files and not osutils.is_inside_any(view_files, relpath):
 
284
 
            raise errors.FileOutsideView(relpath, view_files)