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.repository import _strip_NULL_ghosts
35
from bzrlib import revision as _mod_revision
38
class SmartServerRepositoryRequest(SmartServerRequest):
39
"""Common base class for Repository requests."""
41
def do(self, path, *args):
42
"""Execute a repository request.
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.
48
The actual logic is delegated to self.do_repository_request.
50
:param client_path: The path for the repository as received from the
52
:return: A SmartServerResponse from self.do_repository_request().
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)
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
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()
73
search = repository.get_graph()._make_breadth_first_searcher(
77
next_revs = search.next()
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',)))
94
class SmartServerRepositoryReadLocked(SmartServerRepositoryRequest):
95
"""Calls self.do_readlocked_repository_request."""
97
def do_repository_request(self, repository, *args):
98
"""Read lock a repository for do_readlocked_repository_request."""
99
repository.lock_read()
101
return self.do_readlocked_repository_request(repository, *args)
106
class SmartServerRepositoryGetParentMap(SmartServerRepositoryRequest):
107
"""Bzr 1.2+ - get parent data for revisions during a graph search."""
109
def do_repository_request(self, repository, *revision_ids):
110
"""Get parent details for some revisions.
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.
117
:param repository: The repository to query in.
118
:param revision_ids: The utf8 encoded revision_id to answer for.
120
self._revision_ids = revision_ids
121
return None # Signal that we want a body.
123
def do_body(self, body_bytes):
124
"""Process the current search state and perform the parent lookup.
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
131
repository = self._repository
132
repository.lock_read()
134
return self._do_repository_request(body_bytes)
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:
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)
150
repo_graph = repository.get_graph()
154
next_revs = revision_ids
155
first_loop_done = False
157
queried_revs.update(next_revs)
158
parent_map = repo_graph.get_parent_map(next_revs)
160
for revision_id, parents in parent_map.iteritems():
161
# adjust for the wire
162
if parents == (_mod_revision.NULL_REVISION,):
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:
179
# don't query things we've already queried
180
next_revs.difference_update(queried_revs)
181
first_loop_done = True
183
# sorting trivially puts lexographically similar revision ids together.
185
for revision, parents in sorted(result.items()):
186
lines.append(' '.join((revision, ) + tuple(parents)))
188
return SuccessfulSmartServerResponse(
189
('ok', ), bz2.compress('\n'.join(lines)))
192
class SmartServerRepositoryGetRevisionGraph(SmartServerRepositoryReadLocked):
194
def do_readlocked_repository_request(self, repository, revision_id):
195
"""Return the result of repository.get_revision_graph(revision_id).
197
Deprecated as of bzr 1.4, but supported for older clients.
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.
208
graph = repository.get_graph()
210
search_ids = [revision_id]
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), '')
224
for revision, parents in revision_graph.items():
225
lines.append(' '.join((revision, ) + tuple(parents)))
227
return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
230
class SmartServerRequestHasRevision(SmartServerRepositoryRequest):
232
def do_repository_request(self, repository, revision_id):
233
"""Return ok if a specific revision is in the repository at path.
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
240
if repository.has_revision(revision_id):
241
return SuccessfulSmartServerResponse(('yes', ))
243
return SuccessfulSmartServerResponse(('no', ))
246
class SmartServerRepositoryGatherStats(SmartServerRepositoryRequest):
248
def do_repository_request(self, repository, revid, committers):
249
"""Return the result of repository.gather_stats().
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'.
255
:return: A SmartServerResponse ('ok',), a encoded body looking like
258
latestrev: 345.700 3600
261
But containing only fields returned by the gather_stats() call
264
decoded_revision_id = None
266
decoded_revision_id = revid
267
if committers == 'yes':
268
decoded_committers = True
270
decoded_committers = None
271
stats = repository.gather_stats(decoded_revision_id, decoded_committers)
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']
285
return SuccessfulSmartServerResponse(('ok', ), body)
288
class SmartServerRepositoryIsShared(SmartServerRepositoryRequest):
290
def do_repository_request(self, repository):
291
"""Return the result of repository.is_shared().
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.
297
if repository.is_shared():
298
return SuccessfulSmartServerResponse(('yes', ))
300
return SuccessfulSmartServerResponse(('no', ))
303
class SmartServerRepositoryLockWrite(SmartServerRepositoryRequest):
305
def do_repository_request(self, repository, token=''):
306
# XXX: this probably should not have a token.
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()
323
return SuccessfulSmartServerResponse(('ok', token))
326
class SmartServerRepositoryUnlock(SmartServerRepositoryRequest):
328
def do_repository_request(self, repository, token):
330
repository.lock_write(token=token)
331
except errors.TokenMismatch, e:
332
return FailedSmartServerResponse(('TokenMismatch',))
333
repository.dont_leave_lock_in_place()
335
return SuccessfulSmartServerResponse(('ok',))
338
class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
339
"""Get the raw repository files as a tarball.
341
The returned tarball contains a .bzr control directory which in turn
342
contains a repository.
344
This takes one parameter, compression, which currently must be
347
This is used to implement the Repository.copy_content_into operation.
350
def do_repository_request(self, repository, compression):
351
from bzrlib import osutils
352
tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
354
controldir_name = tmp_dirname + '/.bzr'
355
return self._tarfile_response(controldir_name, compression)
357
osutils.rmtree(tmp_dirname)
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
366
def _tarfile_response(self, tmp_dirname, compression):
367
temp = tempfile.NamedTemporaryFile()
369
self._tarball_of_dir(tmp_dirname, compression, temp.file)
370
# all finished; write the tempfile out to the network
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
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)
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
387
dirname = dirname.encode(sys.getfilesystemencoding())
388
# python's tarball module includes the whole path by default so
390
if not dirname.endswith('.bzr'):
391
raise ValueError(dirname)
392
tarball.add(dirname, '.bzr') # recursive by default