/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: Andrew Bennetts
  • Date: 2008-01-22 05:00:42 UTC
  • mto: This revision was merged to the branch mainline in revision 3197.
  • Revision ID: andrew.bennetts@canonical.com-20080122050042-hh98dtixpfem8dbe
Always include timestamps in the trace file (i.e. remove -Dtimes in favour of having it switched on permanently)

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