~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-10-31 03:15:38 UTC
  • mfrom: (3806.1.1 contrib)
  • Revision ID: pqm@pqm.ubuntu.com-20081031031538-0ih0zi2owevf6fwu
Add contrib/bzr_ssh_path_limiter. (Andrew Bennetts)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Server-side repository related request implmentations."""
 
18
 
 
19
import bz2
 
20
import os
 
21
import sys
 
22
import tempfile
 
23
import tarfile
 
24
 
 
25
from bzrlib import (
 
26
    errors,
 
27
    osutils,
 
28
    )
 
29
from bzrlib.bzrdir import BzrDir
 
30
from bzrlib.smart.request import (
 
31
    FailedSmartServerResponse,
 
32
    SmartServerRequest,
 
33
    SuccessfulSmartServerResponse,
 
34
    )
 
35
from bzrlib.repository import _strip_NULL_ghosts
 
36
from bzrlib import revision as _mod_revision
 
37
 
 
38
 
 
39
class SmartServerRepositoryRequest(SmartServerRequest):
 
40
    """Common base class for Repository requests."""
 
41
 
 
42
    def do(self, path, *args):
 
43
        """Execute a repository request.
 
44
        
 
45
        All Repository requests take a path to the repository as their first
 
46
        argument.  The repository must be at the exact path given by the
 
47
        client - no searching is done.
 
48
 
 
49
        The actual logic is delegated to self.do_repository_request.
 
50
 
 
51
        :param client_path: The path for the repository as received from the
 
52
            client.
 
53
        :return: A SmartServerResponse from self.do_repository_request().
 
54
        """
 
55
        transport = self.transport_from_client_path(path)
 
56
        bzrdir = BzrDir.open_from_transport(transport)
 
57
        # Save the repository for use with do_body.
 
58
        self._repository = bzrdir.open_repository()
 
59
        return self.do_repository_request(self._repository, *args)
 
60
 
 
61
    def do_repository_request(self, repository, *args):
 
62
        """Override to provide an implementation for a verb."""
 
63
        # No-op for verbs that take bodies (None as a result indicates a body
 
64
        # is expected)
 
65
        return None
 
66
 
 
67
    def recreate_search(self, repository, recipe_bytes):
 
68
        lines = recipe_bytes.split('\n')
 
69
        start_keys = set(lines[0].split(' '))
 
70
        exclude_keys = set(lines[1].split(' '))
 
71
        revision_count = int(lines[2])
 
72
        repository.lock_read()
 
73
        try:
 
74
            search = repository.get_graph()._make_breadth_first_searcher(
 
75
                start_keys)
 
76
            while True:
 
77
                try:
 
78
                    next_revs = search.next()
 
79
                except StopIteration:
 
80
                    break
 
81
                search.stop_searching_any(exclude_keys.intersection(next_revs))
 
82
            search_result = search.get_result()
 
83
            if search_result.get_recipe()[2] != revision_count:
 
84
                # we got back a different amount of data than expected, this
 
85
                # gets reported as NoSuchRevision, because less revisions
 
86
                # indicates missing revisions, and more should never happen as
 
87
                # the excludes list considers ghosts and ensures that ghost
 
88
                # filling races are not a problem.
 
89
                return (None, FailedSmartServerResponse(('NoSuchRevision',)))
 
90
            return (search, None)
 
91
        finally:
 
92
            repository.unlock()
 
93
 
 
94
 
 
95
class SmartServerRepositoryReadLocked(SmartServerRepositoryRequest):
 
96
    """Calls self.do_readlocked_repository_request."""
 
97
 
 
98
    def do_repository_request(self, repository, *args):
 
99
        """Read lock a repository for do_readlocked_repository_request."""
 
100
        repository.lock_read()
 
101
        try:
 
102
            return self.do_readlocked_repository_request(repository, *args)
 
103
        finally:
 
104
            repository.unlock()
 
105
 
 
106
 
 
107
class SmartServerRepositoryGetParentMap(SmartServerRepositoryRequest):
 
108
    """Bzr 1.2+ - get parent data for revisions during a graph search."""
 
109
    
 
110
    def do_repository_request(self, repository, *revision_ids):
 
111
        """Get parent details for some revisions.
 
112
        
 
113
        All the parents for revision_ids are returned. Additionally up to 64KB
 
114
        of additional parent data found by performing a breadth first search
 
115
        from revision_ids is returned. The verb takes a body containing the
 
116
        current search state, see do_body for details.
 
117
 
 
118
        :param repository: The repository to query in.
 
119
        :param revision_ids: The utf8 encoded revision_id to answer for.
 
120
        """
 
121
        self._revision_ids = revision_ids
 
122
        return None # Signal that we want a body.
 
123
 
 
124
    def do_body(self, body_bytes):
 
125
        """Process the current search state and perform the parent lookup.
 
126
 
 
127
        :return: A smart server response where the body contains an utf8
 
128
            encoded flattened list of the parents of the revisions (the same
 
129
            format as Repository.get_revision_graph) which has been bz2
 
130
            compressed.
 
131
        """
 
132
        repository = self._repository
 
133
        repository.lock_read()
 
134
        try:
 
135
            return self._do_repository_request(body_bytes)
 
136
        finally:
 
137
            repository.unlock()
 
138
 
 
139
    def _do_repository_request(self, body_bytes):
 
140
        repository = self._repository
 
141
        revision_ids = set(self._revision_ids)
 
142
        search, error = self.recreate_search(repository, body_bytes)
 
143
        if error is not None:
 
144
            return error
 
145
        # TODO might be nice to start up the search again; but thats not
 
146
        # written or tested yet.
 
147
        client_seen_revs = set(search.get_result().get_keys())
 
148
        # Always include the requested ids.
 
149
        client_seen_revs.difference_update(revision_ids)
 
150
        lines = []
 
151
        repo_graph = repository.get_graph()
 
152
        result = {}
 
153
        queried_revs = set()
 
154
        size_so_far = 0
 
155
        next_revs = revision_ids
 
156
        first_loop_done = False
 
157
        while next_revs:
 
158
            queried_revs.update(next_revs)
 
159
            parent_map = repo_graph.get_parent_map(next_revs)
 
160
            next_revs = set()
 
161
            for revision_id, parents in parent_map.iteritems():
 
162
                # adjust for the wire
 
163
                if parents == (_mod_revision.NULL_REVISION,):
 
164
                    parents = ()
 
165
                # prepare the next query
 
166
                next_revs.update(parents)
 
167
                if revision_id not in client_seen_revs:
 
168
                    # Client does not have this revision, give it to it.
 
169
                    # add parents to the result
 
170
                    result[revision_id] = parents
 
171
                    # Approximate the serialized cost of this revision_id.
 
172
                    size_so_far += 2 + len(revision_id) + sum(map(len, parents))
 
173
            # get all the directly asked for parents, and then flesh out to
 
174
            # 64K (compressed) or so. We do one level of depth at a time to
 
175
            # stay in sync with the client. The 250000 magic number is
 
176
            # estimated compression ratio taken from bzr.dev itself.
 
177
            if first_loop_done and size_so_far > 250000:
 
178
                next_revs = set()
 
179
                break
 
180
            # don't query things we've already queried
 
181
            next_revs.difference_update(queried_revs)
 
182
            first_loop_done = True
 
183
 
 
184
        # sorting trivially puts lexographically similar revision ids together.
 
185
        # Compression FTW.
 
186
        for revision, parents in sorted(result.items()):
 
187
            lines.append(' '.join((revision, ) + tuple(parents)))
 
188
 
 
189
        return SuccessfulSmartServerResponse(
 
190
            ('ok', ), bz2.compress('\n'.join(lines)))
 
191
 
 
192
 
 
193
class SmartServerRepositoryGetRevisionGraph(SmartServerRepositoryReadLocked):
 
194
    
 
195
    def do_readlocked_repository_request(self, repository, revision_id):
 
196
        """Return the result of repository.get_revision_graph(revision_id).
 
197
 
 
198
        Deprecated as of bzr 1.4, but supported for older clients.
 
199
        
 
200
        :param repository: The repository to query in.
 
201
        :param revision_id: The utf8 encoded revision_id to get a graph from.
 
202
        :return: A smart server response where the body contains an utf8
 
203
            encoded flattened list of the revision graph.
 
204
        """
 
205
        if not revision_id:
 
206
            revision_id = None
 
207
 
 
208
        lines = []
 
209
        graph = repository.get_graph()
 
210
        if revision_id:
 
211
            search_ids = [revision_id]
 
212
        else:
 
213
            search_ids = repository.all_revision_ids()
 
214
        search = graph._make_breadth_first_searcher(search_ids)
 
215
        transitive_ids = set()
 
216
        map(transitive_ids.update, list(search))
 
217
        parent_map = graph.get_parent_map(transitive_ids)
 
218
        revision_graph = _strip_NULL_ghosts(parent_map)
 
219
        if revision_id and revision_id not in revision_graph:
 
220
            # Note that we return an empty body, rather than omitting the body.
 
221
            # This way the client knows that it can always expect to find a body
 
222
            # in the response for this method, even in the error case.
 
223
            return FailedSmartServerResponse(('nosuchrevision', revision_id), '')
 
224
 
 
225
        for revision, parents in revision_graph.items():
 
226
            lines.append(' '.join((revision, ) + tuple(parents)))
 
227
 
 
228
        return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
 
229
 
 
230
 
 
231
class SmartServerRequestHasRevision(SmartServerRepositoryRequest):
 
232
 
 
233
    def do_repository_request(self, repository, revision_id):
 
234
        """Return ok if a specific revision is in the repository at path.
 
235
 
 
236
        :param repository: The repository to query in.
 
237
        :param revision_id: The utf8 encoded revision_id to lookup.
 
238
        :return: A smart server response of ('ok', ) if the revision is
 
239
            present.
 
240
        """
 
241
        if repository.has_revision(revision_id):
 
242
            return SuccessfulSmartServerResponse(('yes', ))
 
243
        else:
 
244
            return SuccessfulSmartServerResponse(('no', ))
 
245
 
 
246
 
 
247
class SmartServerRepositoryGatherStats(SmartServerRepositoryRequest):
 
248
 
 
249
    def do_repository_request(self, repository, revid, committers):
 
250
        """Return the result of repository.gather_stats().
 
251
 
 
252
        :param repository: The repository to query in.
 
253
        :param revid: utf8 encoded rev id or an empty string to indicate None
 
254
        :param committers: 'yes' or 'no'.
 
255
 
 
256
        :return: A SmartServerResponse ('ok',), a encoded body looking like
 
257
              committers: 1
 
258
              firstrev: 1234.230 0
 
259
              latestrev: 345.700 3600
 
260
              revisions: 2
 
261
 
 
262
              But containing only fields returned by the gather_stats() call
 
263
        """
 
264
        if revid == '':
 
265
            decoded_revision_id = None
 
266
        else:
 
267
            decoded_revision_id = revid
 
268
        if committers == 'yes':
 
269
            decoded_committers = True
 
270
        else:
 
271
            decoded_committers = None
 
272
        stats = repository.gather_stats(decoded_revision_id, decoded_committers)
 
273
 
 
274
        body = ''
 
275
        if stats.has_key('committers'):
 
276
            body += 'committers: %d\n' % stats['committers']
 
277
        if stats.has_key('firstrev'):
 
278
            body += 'firstrev: %.3f %d\n' % stats['firstrev']
 
279
        if stats.has_key('latestrev'):
 
280
             body += 'latestrev: %.3f %d\n' % stats['latestrev']
 
281
        if stats.has_key('revisions'):
 
282
            body += 'revisions: %d\n' % stats['revisions']
 
283
        if stats.has_key('size'):
 
284
            body += 'size: %d\n' % stats['size']
 
285
 
 
286
        return SuccessfulSmartServerResponse(('ok', ), body)
 
287
 
 
288
 
 
289
class SmartServerRepositoryIsShared(SmartServerRepositoryRequest):
 
290
 
 
291
    def do_repository_request(self, repository):
 
292
        """Return the result of repository.is_shared().
 
293
 
 
294
        :param repository: The repository to query in.
 
295
        :return: A smart server response of ('yes', ) if the repository is
 
296
            shared, and ('no', ) if it is not.
 
297
        """
 
298
        if repository.is_shared():
 
299
            return SuccessfulSmartServerResponse(('yes', ))
 
300
        else:
 
301
            return SuccessfulSmartServerResponse(('no', ))
 
302
 
 
303
 
 
304
class SmartServerRepositoryLockWrite(SmartServerRepositoryRequest):
 
305
 
 
306
    def do_repository_request(self, repository, token=''):
 
307
        # XXX: this probably should not have a token.
 
308
        if token == '':
 
309
            token = None
 
310
        try:
 
311
            token = repository.lock_write(token=token)
 
312
        except errors.LockContention, e:
 
313
            return FailedSmartServerResponse(('LockContention',))
 
314
        except errors.UnlockableTransport:
 
315
            return FailedSmartServerResponse(('UnlockableTransport',))
 
316
        except errors.LockFailed, e:
 
317
            return FailedSmartServerResponse(('LockFailed',
 
318
                str(e.lock), str(e.why)))
 
319
        if token is not None:
 
320
            repository.leave_lock_in_place()
 
321
        repository.unlock()
 
322
        if token is None:
 
323
            token = ''
 
324
        return SuccessfulSmartServerResponse(('ok', token))
 
325
 
 
326
 
 
327
class SmartServerRepositoryUnlock(SmartServerRepositoryRequest):
 
328
 
 
329
    def do_repository_request(self, repository, token):
 
330
        try:
 
331
            repository.lock_write(token=token)
 
332
        except errors.TokenMismatch, e:
 
333
            return FailedSmartServerResponse(('TokenMismatch',))
 
334
        repository.dont_leave_lock_in_place()
 
335
        repository.unlock()
 
336
        return SuccessfulSmartServerResponse(('ok',))
 
337
 
 
338
 
 
339
class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
 
340
    """Get the raw repository files as a tarball.
 
341
 
 
342
    The returned tarball contains a .bzr control directory which in turn
 
343
    contains a repository.
 
344
    
 
345
    This takes one parameter, compression, which currently must be 
 
346
    "", "gz", or "bz2".
 
347
 
 
348
    This is used to implement the Repository.copy_content_into operation.
 
349
    """
 
350
 
 
351
    def do_repository_request(self, repository, compression):
 
352
        tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
 
353
        try:
 
354
            controldir_name = tmp_dirname + '/.bzr'
 
355
            return self._tarfile_response(controldir_name, compression)
 
356
        finally:
 
357
            osutils.rmtree(tmp_dirname)
 
358
 
 
359
    def _copy_to_tempdir(self, from_repo):
 
360
        tmp_dirname = osutils.mkdtemp(prefix='tmpbzrclone')
 
361
        tmp_bzrdir = from_repo.bzrdir._format.initialize(tmp_dirname)
 
362
        tmp_repo = from_repo._format.initialize(tmp_bzrdir)
 
363
        from_repo.copy_content_into(tmp_repo)
 
364
        return tmp_dirname, tmp_repo
 
365
 
 
366
    def _tarfile_response(self, tmp_dirname, compression):
 
367
        temp = tempfile.NamedTemporaryFile()
 
368
        try:
 
369
            self._tarball_of_dir(tmp_dirname, compression, temp.file)
 
370
            # all finished; write the tempfile out to the network
 
371
            temp.seek(0)
 
372
            return SuccessfulSmartServerResponse(('ok',), temp.read())
 
373
            # FIXME: Don't read the whole thing into memory here; rather stream
 
374
            # it out from the file onto the network. mbp 20070411
 
375
        finally:
 
376
            temp.close()
 
377
 
 
378
    def _tarball_of_dir(self, dirname, compression, ofile):
 
379
        filename = os.path.basename(ofile.name)
 
380
        tarball = tarfile.open(fileobj=ofile, name=filename,
 
381
            mode='w|' + compression)
 
382
        try:
 
383
            # The tarball module only accepts ascii names, and (i guess)
 
384
            # packs them with their 8bit names.  We know all the files
 
385
            # within the repository have ASCII names so the should be safe
 
386
            # to pack in.
 
387
            dirname = dirname.encode(sys.getfilesystemencoding())
 
388
            # python's tarball module includes the whole path by default so
 
389
            # override it
 
390
            if not dirname.endswith('.bzr'):
 
391
                raise ValueError(dirname)
 
392
            tarball.add(dirname, '.bzr') # recursive by default
 
393
        finally:
 
394
            tarball.close()