/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: John Arbash Meinel
  • Date: 2008-11-25 18:51:48 UTC
  • mto: This revision was merged to the branch mainline in revision 3854.
  • Revision ID: john@arbash-meinel.com-20081125185148-jsfkqnzfjjqsleds
It seems we have some direct tests that don't use strings and expect a value error as well.

They would be sanitized later on by Revision. We could use that code, but this test
depends on the serializer, which Revision wouldn't know about.

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()