/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: Daniel Watkins
  • Date: 2008-03-09 18:08:36 UTC
  • mto: This revision was merged to the branch mainline in revision 3269.
  • Revision ID: d.m.watkins@warwick.ac.uk-20080309180836-y7wwg4o22e2tegjf
Added test to ensure that passing a range of revisions errors.

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