~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

  • Committer: Aaron Bentley
  • Date: 2007-02-06 14:52:16 UTC
  • mfrom: (2266 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2268.
  • Revision ID: abentley@panoramicfeedback.com-20070206145216-fcpi8o3ufvuzwbp9
Merge bzr.dev

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
 
 
261
 
              But containing only fields returned by the gather_stats() call
262
 
        """
263
 
        if revid == '':
264
 
            decoded_revision_id = None
265
 
        else:
266
 
            decoded_revision_id = revid
267
 
        if committers == 'yes':
268
 
            decoded_committers = True
269
 
        else:
270
 
            decoded_committers = None
271
 
        stats = repository.gather_stats(decoded_revision_id, decoded_committers)
272
 
 
273
 
        body = ''
274
 
        if stats.has_key('committers'):
275
 
            body += 'committers: %d\n' % stats['committers']
276
 
        if stats.has_key('firstrev'):
277
 
            body += 'firstrev: %.3f %d\n' % stats['firstrev']
278
 
        if stats.has_key('latestrev'):
279
 
             body += 'latestrev: %.3f %d\n' % stats['latestrev']
280
 
        if stats.has_key('revisions'):
281
 
            body += 'revisions: %d\n' % stats['revisions']
282
 
        if stats.has_key('size'):
283
 
            body += 'size: %d\n' % stats['size']
284
 
 
285
 
        return SuccessfulSmartServerResponse(('ok', ), body)
286
 
 
287
 
 
288
 
class SmartServerRepositoryIsShared(SmartServerRepositoryRequest):
289
 
 
290
 
    def do_repository_request(self, repository):
291
 
        """Return the result of repository.is_shared().
292
 
 
293
 
        :param repository: The repository to query in.
294
 
        :return: A smart server response of ('yes', ) if the repository is
295
 
            shared, and ('no', ) if it is not.
296
 
        """
297
 
        if repository.is_shared():
298
 
            return SuccessfulSmartServerResponse(('yes', ))
299
 
        else:
300
 
            return SuccessfulSmartServerResponse(('no', ))
301
 
 
302
 
 
303
 
class SmartServerRepositoryLockWrite(SmartServerRepositoryRequest):
304
 
 
305
 
    def do_repository_request(self, repository, token=''):
306
 
        # XXX: this probably should not have a token.
307
 
        if token == '':
308
 
            token = None
309
 
        try:
310
 
            token = repository.lock_write(token=token)
311
 
        except errors.LockContention, e:
312
 
            return FailedSmartServerResponse(('LockContention',))
313
 
        except errors.UnlockableTransport:
314
 
            return FailedSmartServerResponse(('UnlockableTransport',))
315
 
        except errors.LockFailed, e:
316
 
            return FailedSmartServerResponse(('LockFailed',
317
 
                str(e.lock), str(e.why)))
318
 
        if token is not None:
319
 
            repository.leave_lock_in_place()
320
 
        repository.unlock()
321
 
        if token is None:
322
 
            token = ''
323
 
        return SuccessfulSmartServerResponse(('ok', token))
324
 
 
325
 
 
326
 
class SmartServerRepositoryUnlock(SmartServerRepositoryRequest):
327
 
 
328
 
    def do_repository_request(self, repository, token):
329
 
        try:
330
 
            repository.lock_write(token=token)
331
 
        except errors.TokenMismatch, e:
332
 
            return FailedSmartServerResponse(('TokenMismatch',))
333
 
        repository.dont_leave_lock_in_place()
334
 
        repository.unlock()
335
 
        return SuccessfulSmartServerResponse(('ok',))
336
 
 
337
 
 
338
 
class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
339
 
    """Get the raw repository files as a tarball.
340
 
 
341
 
    The returned tarball contains a .bzr control directory which in turn
342
 
    contains a repository.
343
 
    
344
 
    This takes one parameter, compression, which currently must be 
345
 
    "", "gz", or "bz2".
346
 
 
347
 
    This is used to implement the Repository.copy_content_into operation.
348
 
    """
349
 
 
350
 
    def do_repository_request(self, repository, compression):
351
 
        from bzrlib import osutils
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 = tempfile.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 it
374
 
            # 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()