1
# Copyright (C) 2006, 2007 Canonical Ltd
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.
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.
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
17
"""Server-side repository related request implmentations."""
20
from cStringIO import StringIO
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,
32
SuccessfulSmartServerResponse,
34
from bzrlib import revision as _mod_revision
37
class SmartServerRepositoryRequest(SmartServerRequest):
38
"""Common base class for Repository requests."""
40
def do(self, path, *args):
41
"""Execute a repository request.
43
The repository must be at the exact path - no searching is done.
45
The actual logic is delegated to self.do_repository_request.
47
:param path: The path for the repository.
48
:return: A smart server from self.do_repository_request().
50
transport = self._backing_transport.clone(path)
51
bzrdir = BzrDir.open_from_transport(transport)
52
# Save the repository for use with do_body.
53
self._repository = bzrdir.open_repository()
54
return self.do_repository_request(self._repository, *args)
56
def do_repository_request(self, repository, *args):
57
"""Override to provide an implementation for a verb."""
58
# No-op for verbs that take bodies (None as a result indicates a body
62
def recreate_search(self, repository, recipe_bytes):
63
lines = recipe_bytes.split('\n')
64
start_keys = set(lines[0].split(' '))
65
exclude_keys = set(lines[1].split(' '))
66
revision_count = int(lines[2])
67
repository.lock_read()
69
search = repository.get_graph()._make_breadth_first_searcher(
73
next_revs = search.next()
76
search.stop_searching_any(exclude_keys.intersection(next_revs))
77
search_result = search.get_result()
78
if search_result.get_recipe()[2] != revision_count:
79
# we got back a different amount of data than expected, this
80
# gets reported as NoSuchRevision, because less revisions
81
# indicates missing revisions, and more should never happen as
82
# the excludes list considers ghosts and ensures that ghost
83
# filling races are not a problem.
84
return (None, FailedSmartServerResponse(('NoSuchRevision',)))
90
class SmartServerRepositoryGetParentMap(SmartServerRepositoryRequest):
91
"""Bzr 1.2+ - get parent data for revisions during a graph search."""
93
def do_repository_request(self, repository, *revision_ids):
94
"""Get parent details for some revisions.
96
All the parents for revision_ids are returned. Additionally up to 64KB
97
of additional parent data found by performing a breadth first search
98
from revision_ids is returned. The verb takes a body containing the
99
current search state, see do_body for details.
101
:param repository: The repository to query in.
102
:param revision_ids: The utf8 encoded revision_id to answer for.
104
self._revision_ids = revision_ids
105
return None # Signal that we want a body.
107
def do_body(self, body_bytes):
108
"""Process the current search state and perform the parent lookup.
110
:return: A smart server response where the body contains an utf8
111
encoded flattened list of the parents of the revisions (the same
112
format as Repository.get_revision_graph) which has been bz2
115
repository = self._repository
116
repository.lock_read()
118
return self._do_repository_request(body_bytes)
122
def _do_repository_request(self, body_bytes):
123
repository = self._repository
124
revision_ids = set(self._revision_ids)
125
search, error = self.recreate_search(repository, body_bytes)
126
if error is not None:
128
# TODO might be nice to start up the search again; but thats not
129
# written or tested yet.
130
client_seen_revs = set(search.get_result().get_keys())
131
# Always include the requested ids.
132
client_seen_revs.difference_update(revision_ids)
134
repo_graph = repository.get_graph()
138
next_revs = revision_ids
139
first_loop_done = False
141
queried_revs.update(next_revs)
142
parent_map = repo_graph.get_parent_map(next_revs)
144
for revision_id, parents in parent_map.iteritems():
145
# adjust for the wire
146
if parents == (_mod_revision.NULL_REVISION,):
148
# prepare the next query
149
next_revs.update(parents)
150
if revision_id not in client_seen_revs:
151
# Client does not have this revision, give it to it.
152
# add parents to the result
153
result[revision_id] = parents
154
# Approximate the serialized cost of this revision_id.
155
size_so_far += 2 + len(revision_id) + sum(map(len, parents))
156
# get all the directly asked for parents, and then flesh out to
157
# 64K (compressed) or so. We do one level of depth at a time to
158
# stay in sync with the client. The 250000 magic number is
159
# estimated compression ratio taken from bzr.dev itself.
160
if first_loop_done and size_so_far > 250000:
163
# don't query things we've already queried
164
next_revs.difference_update(queried_revs)
165
first_loop_done = True
167
# sorting trivially puts lexographically similar revision ids together.
169
for revision, parents in sorted(result.items()):
170
lines.append(' '.join((revision, ) + tuple(parents)))
172
return SuccessfulSmartServerResponse(
173
('ok', ), bz2.compress('\n'.join(lines)))
176
class SmartServerRepositoryGetRevisionGraph(SmartServerRepositoryRequest):
178
def do_repository_request(self, repository, revision_id):
179
"""Return the result of repository.get_revision_graph(revision_id).
181
:param repository: The repository to query in.
182
:param revision_id: The utf8 encoded revision_id to get a graph from.
183
:return: A smart server response where the body contains an utf8
184
encoded flattened list of the revision graph.
191
revision_graph = repository.get_revision_graph(revision_id)
192
except errors.NoSuchRevision:
193
# Note that we return an empty body, rather than omitting the body.
194
# This way the client knows that it can always expect to find a body
195
# in the response for this method, even in the error case.
196
return FailedSmartServerResponse(('nosuchrevision', revision_id), '')
198
for revision, parents in revision_graph.items():
199
lines.append(' '.join((revision, ) + tuple(parents)))
201
return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
204
class SmartServerRequestHasRevision(SmartServerRepositoryRequest):
206
def do_repository_request(self, repository, revision_id):
207
"""Return ok if a specific revision is in the repository at path.
209
:param repository: The repository to query in.
210
:param revision_id: The utf8 encoded revision_id to lookup.
211
:return: A smart server response of ('ok', ) if the revision is
214
if repository.has_revision(revision_id):
215
return SuccessfulSmartServerResponse(('yes', ))
217
return SuccessfulSmartServerResponse(('no', ))
220
class SmartServerRepositoryGatherStats(SmartServerRepositoryRequest):
222
def do_repository_request(self, repository, revid, committers):
223
"""Return the result of repository.gather_stats().
225
:param repository: The repository to query in.
226
:param revid: utf8 encoded rev id or an empty string to indicate None
227
:param committers: 'yes' or 'no'.
229
:return: A SmartServerResponse ('ok',), a encoded body looking like
232
latestrev: 345.700 3600
236
But containing only fields returned by the gather_stats() call
239
decoded_revision_id = None
241
decoded_revision_id = revid
242
if committers == 'yes':
243
decoded_committers = True
245
decoded_committers = None
246
stats = repository.gather_stats(decoded_revision_id, decoded_committers)
249
if stats.has_key('committers'):
250
body += 'committers: %d\n' % stats['committers']
251
if stats.has_key('firstrev'):
252
body += 'firstrev: %.3f %d\n' % stats['firstrev']
253
if stats.has_key('latestrev'):
254
body += 'latestrev: %.3f %d\n' % stats['latestrev']
255
if stats.has_key('revisions'):
256
body += 'revisions: %d\n' % stats['revisions']
257
if stats.has_key('size'):
258
body += 'size: %d\n' % stats['size']
260
return SuccessfulSmartServerResponse(('ok', ), body)
263
class SmartServerRepositoryIsShared(SmartServerRepositoryRequest):
265
def do_repository_request(self, repository):
266
"""Return the result of repository.is_shared().
268
:param repository: The repository to query in.
269
:return: A smart server response of ('yes', ) if the repository is
270
shared, and ('no', ) if it is not.
272
if repository.is_shared():
273
return SuccessfulSmartServerResponse(('yes', ))
275
return SuccessfulSmartServerResponse(('no', ))
278
class SmartServerRepositoryLockWrite(SmartServerRepositoryRequest):
280
def do_repository_request(self, repository, token=''):
281
# XXX: this probably should not have a token.
285
token = repository.lock_write(token=token)
286
except errors.LockContention, e:
287
return FailedSmartServerResponse(('LockContention',))
288
except errors.UnlockableTransport:
289
return FailedSmartServerResponse(('UnlockableTransport',))
290
except errors.LockFailed, e:
291
return FailedSmartServerResponse(('LockFailed',
292
str(e.lock), str(e.why)))
293
if token is not None:
294
repository.leave_lock_in_place()
298
return SuccessfulSmartServerResponse(('ok', token))
301
class SmartServerRepositoryUnlock(SmartServerRepositoryRequest):
303
def do_repository_request(self, repository, token):
305
repository.lock_write(token=token)
306
except errors.TokenMismatch, e:
307
return FailedSmartServerResponse(('TokenMismatch',))
308
repository.dont_leave_lock_in_place()
310
return SuccessfulSmartServerResponse(('ok',))
313
class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
314
"""Get the raw repository files as a tarball.
316
The returned tarball contains a .bzr control directory which in turn
317
contains a repository.
319
This takes one parameter, compression, which currently must be
322
This is used to implement the Repository.copy_content_into operation.
325
def do_repository_request(self, repository, compression):
326
from bzrlib import osutils
327
repo_transport = repository.control_files._transport
328
tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
330
controldir_name = tmp_dirname + '/.bzr'
331
return self._tarfile_response(controldir_name, compression)
333
osutils.rmtree(tmp_dirname)
335
def _copy_to_tempdir(self, from_repo):
336
tmp_dirname = tempfile.mkdtemp(prefix='tmpbzrclone')
337
tmp_bzrdir = from_repo.bzrdir._format.initialize(tmp_dirname)
338
tmp_repo = from_repo._format.initialize(tmp_bzrdir)
339
from_repo.copy_content_into(tmp_repo)
340
return tmp_dirname, tmp_repo
342
def _tarfile_response(self, tmp_dirname, compression):
343
temp = tempfile.NamedTemporaryFile()
345
self._tarball_of_dir(tmp_dirname, compression, temp.file)
346
# all finished; write the tempfile out to the network
348
return SuccessfulSmartServerResponse(('ok',), temp.read())
349
# FIXME: Don't read the whole thing into memory here; rather stream it
350
# out from the file onto the network. mbp 20070411
354
def _tarball_of_dir(self, dirname, compression, ofile):
355
filename = os.path.basename(ofile.name)
356
tarball = tarfile.open(fileobj=ofile, name=filename,
357
mode='w|' + compression)
359
# The tarball module only accepts ascii names, and (i guess)
360
# packs them with their 8bit names. We know all the files
361
# within the repository have ASCII names so the should be safe
363
dirname = dirname.encode(sys.getfilesystemencoding())
364
# python's tarball module includes the whole path by default so
366
assert dirname.endswith('.bzr')
367
tarball.add(dirname, '.bzr') # recursive by default
372
class SmartServerRepositoryStreamKnitDataForRevisions(SmartServerRepositoryRequest):
373
"""Bzr <= 1.1 streaming pull, buffers all data on server."""
375
def do_repository_request(self, repository, *revision_ids):
376
repository.lock_read()
378
return self._do_repository_request(repository, revision_ids)
382
def _do_repository_request(self, repository, revision_ids):
383
stream = repository.get_data_stream_for_search(
384
repository.revision_ids_to_search_result(set(revision_ids)))
386
pack = ContainerSerialiser()
387
buffer.write(pack.begin())
389
for name_tuple, bytes in stream:
390
buffer.write(pack.bytes_record(bytes, [name_tuple]))
391
except errors.RevisionNotPresent, e:
392
return FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
393
buffer.write(pack.end())
394
return SuccessfulSmartServerResponse(('ok',), buffer.getvalue())
397
class SmartServerRepositoryStreamRevisionsChunked(SmartServerRepositoryRequest):
398
"""Bzr 1.1+ streaming pull."""
400
def do_body(self, body_bytes):
401
repository = self._repository
402
repository.lock_read()
404
search, error = self.recreate_search(repository, body_bytes)
405
if error is not None:
407
stream = repository.get_data_stream_for_search(search.get_result())
409
# On non-error, unlocking is done by the body stream handler.
412
return SuccessfulSmartServerResponse(('ok',),
413
body_stream=self.body_stream(stream, repository))
415
def body_stream(self, stream, repository):
416
pack = ContainerSerialiser()
419
for name_tuple, bytes in stream:
420
yield pack.bytes_record(bytes, [name_tuple])
421
except errors.RevisionNotPresent, e:
422
# This shouldn't be able to happen, but as we don't buffer
423
# everything it can in theory happen.
424
yield FailedSmartServerResponse(('NoSuchRevision', e.revision_id))