~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-06-20 01:09:18 UTC
  • mfrom: (3505.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080620010918-64z4xylh1ap5hgyf
Accept user names with @s in URLs (Neil Martinsen-Burrell)

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