~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/repository.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

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