~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

Update test support, and remove deprecated functions pullable_revisions and get_intervening_revisions.

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
 
from cStringIO import StringIO
21
 
import os
22
 
import sys
23
 
import tempfile
24
 
import tarfile
25
 
 
26
 
from bzrlib import errors
27
 
from bzrlib.bzrdir import BzrDir
28
 
from bzrlib.pack import ContainerSerialiser
29
 
from bzrlib.smart.request import (
30
 
    FailedSmartServerResponse,
31
 
    SmartServerRequest,
32
 
    SuccessfulSmartServerResponse,
33
 
    )
34
 
from bzrlib.repository import _strip_NULL_ghosts
35
 
from bzrlib import revision as _mod_revision
36
 
 
37
 
 
38
 
class SmartServerRepositoryRequest(SmartServerRequest):
39
 
    """Common base class for Repository requests."""
40
 
 
41
 
    def do(self, path, *args):
42
 
        """Execute a repository request.
43
 
        
44
 
        All Repository requests take a path to the repository as their first
45
 
        argument.  The repository must be at the exact path given by the
46
 
        client - no searching is done.
47
 
 
48
 
        The actual logic is delegated to self.do_repository_request.
49
 
 
50
 
        :param client_path: The path for the repository as received from the
51
 
            client.
52
 
        :return: A SmartServerResponse from self.do_repository_request().
53
 
        """
54
 
        transport = self.transport_from_client_path(path)
55
 
        bzrdir = BzrDir.open_from_transport(transport)
56
 
        # Save the repository for use with do_body.
57
 
        self._repository = bzrdir.open_repository()
58
 
        return self.do_repository_request(self._repository, *args)
59
 
 
60
 
    def do_repository_request(self, repository, *args):
61
 
        """Override to provide an implementation for a verb."""
62
 
        # No-op for verbs that take bodies (None as a result indicates a body
63
 
        # is expected)
64
 
        return None
65
 
 
66
 
    def recreate_search(self, repository, recipe_bytes):
67
 
        lines = recipe_bytes.split('\n')
68
 
        start_keys = set(lines[0].split(' '))
69
 
        exclude_keys = set(lines[1].split(' '))
70
 
        revision_count = int(lines[2])
71
 
        repository.lock_read()
72
 
        try:
73
 
            search = repository.get_graph()._make_breadth_first_searcher(
74
 
                start_keys)
75
 
            while True:
76
 
                try:
77
 
                    next_revs = search.next()
78
 
                except StopIteration:
79
 
                    break
80
 
                search.stop_searching_any(exclude_keys.intersection(next_revs))
81
 
            search_result = search.get_result()
82
 
            if search_result.get_recipe()[2] != revision_count:
83
 
                # we got back a different amount of data than expected, this
84
 
                # gets reported as NoSuchRevision, because less revisions
85
 
                # indicates missing revisions, and more should never happen as
86
 
                # the excludes list considers ghosts and ensures that ghost
87
 
                # filling races are not a problem.
88
 
                return (None, FailedSmartServerResponse(('NoSuchRevision',)))
89
 
            return (search, None)
90
 
        finally:
91
 
            repository.unlock()
92
 
 
93
 
 
94
 
class SmartServerRepositoryReadLocked(SmartServerRepositoryRequest):
95
 
    """Calls self.do_readlocked_repository_request."""
96
 
 
97
 
    def do_repository_request(self, repository, *args):
98
 
        """Read lock a repository for do_readlocked_repository_request."""
99
 
        repository.lock_read()
100
 
        try:
101
 
            return self.do_readlocked_repository_request(repository, *args)
102
 
        finally:
103
 
            repository.unlock()
104
 
 
105
 
 
106
 
class SmartServerRepositoryGetParentMap(SmartServerRepositoryRequest):
107
 
    """Bzr 1.2+ - get parent data for revisions during a graph search."""
108
 
    
109
 
    def do_repository_request(self, repository, *revision_ids):
110
 
        """Get parent details for some revisions.
111
 
        
112
 
        All the parents for revision_ids are returned. Additionally up to 64KB
113
 
        of additional parent data found by performing a breadth first search
114
 
        from revision_ids is returned. The verb takes a body containing the
115
 
        current search state, see do_body for details.
116
 
 
117
 
        :param repository: The repository to query in.
118
 
        :param revision_ids: The utf8 encoded revision_id to answer for.
119
 
        """
120
 
        self._revision_ids = revision_ids
121
 
        return None # Signal that we want a body.
122
 
 
123
 
    def do_body(self, body_bytes):
124
 
        """Process the current search state and perform the parent lookup.
125
 
 
126
 
        :return: A smart server response where the body contains an utf8
127
 
            encoded flattened list of the parents of the revisions (the same
128
 
            format as Repository.get_revision_graph) which has been bz2
129
 
            compressed.
130
 
        """
131
 
        repository = self._repository
132
 
        repository.lock_read()
133
 
        try:
134
 
            return self._do_repository_request(body_bytes)
135
 
        finally:
136
 
            repository.unlock()
137
 
 
138
 
    def _do_repository_request(self, body_bytes):
139
 
        repository = self._repository
140
 
        revision_ids = set(self._revision_ids)
141
 
        search, error = self.recreate_search(repository, body_bytes)
142
 
        if error is not None:
143
 
            return error
144
 
        # TODO might be nice to start up the search again; but thats not
145
 
        # written or tested yet.
146
 
        client_seen_revs = set(search.get_result().get_keys())
147
 
        # Always include the requested ids.
148
 
        client_seen_revs.difference_update(revision_ids)
149
 
        lines = []
150
 
        repo_graph = repository.get_graph()
151
 
        result = {}
152
 
        queried_revs = set()
153
 
        size_so_far = 0
154
 
        next_revs = revision_ids
155
 
        first_loop_done = False
156
 
        while next_revs:
157
 
            queried_revs.update(next_revs)
158
 
            parent_map = repo_graph.get_parent_map(next_revs)
159
 
            next_revs = set()
160
 
            for revision_id, parents in parent_map.iteritems():
161
 
                # adjust for the wire
162
 
                if parents == (_mod_revision.NULL_REVISION,):
163
 
                    parents = ()
164
 
                # prepare the next query
165
 
                next_revs.update(parents)
166
 
                if revision_id not in client_seen_revs:
167
 
                    # Client does not have this revision, give it to it.
168
 
                    # add parents to the result
169
 
                    result[revision_id] = parents
170
 
                    # Approximate the serialized cost of this revision_id.
171
 
                    size_so_far += 2 + len(revision_id) + sum(map(len, parents))
172
 
            # get all the directly asked for parents, and then flesh out to
173
 
            # 64K (compressed) or so. We do one level of depth at a time to
174
 
            # stay in sync with the client. The 250000 magic number is
175
 
            # estimated compression ratio taken from bzr.dev itself.
176
 
            if first_loop_done and size_so_far > 250000:
177
 
                next_revs = set()
178
 
                break
179
 
            # don't query things we've already queried
180
 
            next_revs.difference_update(queried_revs)
181
 
            first_loop_done = True
182
 
 
183
 
        # sorting trivially puts lexographically similar revision ids together.
184
 
        # Compression FTW.
185
 
        for revision, parents in sorted(result.items()):
186
 
            lines.append(' '.join((revision, ) + tuple(parents)))
187
 
 
188
 
        return SuccessfulSmartServerResponse(
189
 
            ('ok', ), bz2.compress('\n'.join(lines)))
190
 
 
191
 
 
192
 
class SmartServerRepositoryGetRevisionGraph(SmartServerRepositoryReadLocked):
193
 
    
194
 
    def do_readlocked_repository_request(self, repository, revision_id):
195
 
        """Return the result of repository.get_revision_graph(revision_id).
196
 
 
197
 
        Deprecated as of bzr 1.4, but supported for older clients.
198
 
        
199
 
        :param repository: The repository to query in.
200
 
        :param revision_id: The utf8 encoded revision_id to get a graph from.
201
 
        :return: A smart server response where the body contains an utf8
202
 
            encoded flattened list of the revision graph.
203
 
        """
204
 
        if not revision_id:
205
 
            revision_id = None
206
 
 
207
 
        lines = []
208
 
        graph = repository.get_graph()
209
 
        if revision_id:
210
 
            search_ids = [revision_id]
211
 
        else:
212
 
            search_ids = repository.all_revision_ids()
213
 
        search = graph._make_breadth_first_searcher(search_ids)
214
 
        transitive_ids = set()
215
 
        map(transitive_ids.update, list(search))
216
 
        parent_map = graph.get_parent_map(transitive_ids)
217
 
        revision_graph = _strip_NULL_ghosts(parent_map)
218
 
        if revision_id and revision_id not in revision_graph:
219
 
            # Note that we return an empty body, rather than omitting the body.
220
 
            # This way the client knows that it can always expect to find a body
221
 
            # in the response for this method, even in the error case.
222
 
            return FailedSmartServerResponse(('nosuchrevision', revision_id), '')
223
 
 
224
 
        for revision, parents in revision_graph.items():
225
 
            lines.append(' '.join((revision, ) + tuple(parents)))
226
 
 
227
 
        return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
228
 
 
229
 
 
230
 
class SmartServerRequestHasRevision(SmartServerRepositoryRequest):
231
 
 
232
 
    def do_repository_request(self, repository, revision_id):
233
 
        """Return ok if a specific revision is in the repository at path.
234
 
 
235
 
        :param repository: The repository to query in.
236
 
        :param revision_id: The utf8 encoded revision_id to lookup.
237
 
        :return: A smart server response of ('ok', ) if the revision is
238
 
            present.
239
 
        """
240
 
        if repository.has_revision(revision_id):
241
 
            return SuccessfulSmartServerResponse(('yes', ))
242
 
        else:
243
 
            return SuccessfulSmartServerResponse(('no', ))
244
 
 
245
 
 
246
 
class SmartServerRepositoryGatherStats(SmartServerRepositoryRequest):
247
 
 
248
 
    def do_repository_request(self, repository, revid, committers):
249
 
        """Return the result of repository.gather_stats().
250
 
 
251
 
        :param repository: The repository to query in.
252
 
        :param revid: utf8 encoded rev id or an empty string to indicate None
253
 
        :param committers: 'yes' or 'no'.
254
 
 
255
 
        :return: A SmartServerResponse ('ok',), a encoded body looking like
256
 
              committers: 1
257
 
              firstrev: 1234.230 0
258
 
              latestrev: 345.700 3600
259
 
              revisions: 2
260
 
              size:45
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
 
        from bzrlib import osutils
353
 
        tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
354
 
        try:
355
 
            controldir_name = tmp_dirname + '/.bzr'
356
 
            return self._tarfile_response(controldir_name, compression)
357
 
        finally:
358
 
            osutils.rmtree(tmp_dirname)
359
 
 
360
 
    def _copy_to_tempdir(self, from_repo):
361
 
        tmp_dirname = tempfile.mkdtemp(prefix='tmpbzrclone')
362
 
        tmp_bzrdir = from_repo.bzrdir._format.initialize(tmp_dirname)
363
 
        tmp_repo = from_repo._format.initialize(tmp_bzrdir)
364
 
        from_repo.copy_content_into(tmp_repo)
365
 
        return tmp_dirname, tmp_repo
366
 
 
367
 
    def _tarfile_response(self, tmp_dirname, compression):
368
 
        temp = tempfile.NamedTemporaryFile()
369
 
        try:
370
 
            self._tarball_of_dir(tmp_dirname, compression, temp.file)
371
 
            # all finished; write the tempfile out to the network
372
 
            temp.seek(0)
373
 
            return SuccessfulSmartServerResponse(('ok',), temp.read())
374
 
            # FIXME: Don't read the whole thing into memory here; rather stream it
375
 
            # out from the file onto the network. mbp 20070411
376
 
        finally:
377
 
            temp.close()
378
 
 
379
 
    def _tarball_of_dir(self, dirname, compression, ofile):
380
 
        filename = os.path.basename(ofile.name)
381
 
        tarball = tarfile.open(fileobj=ofile, name=filename,
382
 
            mode='w|' + compression)
383
 
        try:
384
 
            # The tarball module only accepts ascii names, and (i guess)
385
 
            # packs them with their 8bit names.  We know all the files
386
 
            # within the repository have ASCII names so the should be safe
387
 
            # to pack in.
388
 
            dirname = dirname.encode(sys.getfilesystemencoding())
389
 
            # python's tarball module includes the whole path by default so
390
 
            # override it
391
 
            if not dirname.endswith('.bzr'):
392
 
                raise ValueError(dirname)
393
 
            tarball.add(dirname, '.bzr') # recursive by default
394
 
        finally:
395
 
            tarball.close()
396
 
 
397
 
 
398
 
class SmartServerRepositoryStreamKnitDataForRevisions(SmartServerRepositoryRequest):
399
 
    """Bzr <= 1.1 streaming pull, buffers all data on server."""
400
 
 
401
 
    def do_repository_request(self, repository, *revision_ids):
402
 
        repository.lock_read()
403
 
        try:
404
 
            return self._do_repository_request(repository, revision_ids)
405
 
        finally:
406
 
            repository.unlock()
407
 
 
408
 
    def _do_repository_request(self, repository, revision_ids):
409
 
        stream = repository.get_data_stream_for_search(
410
 
            repository.revision_ids_to_search_result(set(revision_ids)))
411
 
        buffer = StringIO()
412
 
        pack = ContainerSerialiser()
413
 
        buffer.write(pack.begin())
414
 
        try:
415
 
            for name_tuple, bytes in stream:
416
 
                buffer.write(pack.bytes_record(bytes, [name_tuple]))
417
 
        except errors.RevisionNotPresent, e:
418
 
            return FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
419
 
        buffer.write(pack.end())
420
 
        return SuccessfulSmartServerResponse(('ok',), buffer.getvalue())
421
 
 
422
 
 
423
 
class SmartServerRepositoryStreamRevisionsChunked(SmartServerRepositoryRequest):
424
 
    """Bzr 1.1+ streaming pull."""
425
 
 
426
 
    def do_body(self, body_bytes):
427
 
        repository = self._repository
428
 
        repository.lock_read()
429
 
        try:
430
 
            search, error = self.recreate_search(repository, body_bytes)
431
 
            if error is not None:
432
 
                repository.unlock()
433
 
                return error
434
 
            stream = repository.get_data_stream_for_search(search.get_result())
435
 
        except Exception:
436
 
            # On non-error, unlocking is done by the body stream handler.
437
 
            repository.unlock()
438
 
            raise
439
 
        return SuccessfulSmartServerResponse(('ok',),
440
 
            body_stream=self.body_stream(stream, repository))
441
 
 
442
 
    def body_stream(self, stream, repository):
443
 
        pack = ContainerSerialiser()
444
 
        yield pack.begin()
445
 
        try:
446
 
            try:
447
 
                for name_tuple, bytes in stream:
448
 
                    yield pack.bytes_record(bytes, [name_tuple])
449
 
            except:
450
 
                # Undo the lock_read that that happens once the iterator from
451
 
                # get_data_stream is started.
452
 
                repository.unlock()
453
 
                raise
454
 
        except errors.RevisionNotPresent, e:
455
 
            # This shouldn't be able to happen, but as we don't buffer
456
 
            # everything it can in theory happen.
457
 
            repository.unlock()
458
 
            yield FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
459
 
        else:
460
 
            repository.unlock()
461
 
            pack.end()
462