~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

  • Committer: Robert Collins
  • Date: 2005-08-23 06:52:09 UTC
  • mto: (974.1.50) (1185.1.10) (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1139.
  • Revision ID: robertc@robertcollins.net-20050823065209-81cd5962c401751b
move io redirection into each test case from the global runner

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