/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-03-28 06:42:20 UTC
  • mfrom: (3287.6.9 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080328064220-ongijg78bfqhvbay
Deprecate a number of VersionedFile method calls,
        and Repository.get_revision_graph. (Robert Collins)

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