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."""
19
from cStringIO import StringIO
25
from bzrlib import errors
26
from bzrlib.bzrdir import BzrDir
27
from bzrlib.pack import ContainerSerialiser
28
from bzrlib.smart.request import (
29
FailedSmartServerResponse,
31
SuccessfulSmartServerResponse,
33
from bzrlib import revision as _mod_revision
36
class SmartServerRepositoryRequest(SmartServerRequest):
37
"""Common base class for Repository requests."""
39
def do(self, path, *args):
40
"""Execute a repository request.
42
The repository must be at the exact path - no searching is done.
44
The actual logic is delegated to self.do_repository_request.
46
:param path: The path for the repository.
47
:return: A smart server from self.do_repository_request().
49
transport = self._backing_transport.clone(path)
50
bzrdir = BzrDir.open_from_transport(transport)
51
repository = bzrdir.open_repository()
52
return self.do_repository_request(repository, *args)
55
class SmartServerRepositoryGetParentMap(SmartServerRepositoryRequest):
57
def do_repository_request(self, repository, *revision_ids):
58
repository.lock_read()
60
return self._do_repository_request(repository, revision_ids)
64
def _do_repository_request(self, repository, revision_ids):
65
"""Get parent details for some revisions.
67
All the parents for revision_ids are returned. Additionally up to 64KB
68
of additional parent data found by performing a breadth first search
69
from revision_ids is returned.
71
:param repository: The repository to query in.
72
:param revision_ids: The utf8 encoded revision_id to answer for.
73
:return: A smart server response where the body contains an utf8
74
encoded flattened list of the parents of the revisions, (the same
75
format as Repository.get_revision_graph).
78
repo_graph = repository.get_graph()
82
next_revs = revision_ids
83
first_loop_done = False
85
queried_revs.update(next_revs)
86
parent_map = repo_graph.get_parent_map(next_revs)
88
for revision_id, parents in parent_map.iteritems():
90
if parents == (_mod_revision.NULL_REVISION,):
92
# add parents to the result
93
result[revision_id] = parents
94
# prepare the next query
95
next_revs.update(parents)
96
# Approximate the serialized cost of this revision_id.
97
size_so_far += 2 + len(revision_id) + sum(map(len, parents))
98
# get all the directly asked for parents, and then flesh out to
100
if first_loop_done and size_so_far > 65000:
103
# don't query things we've already queried
104
next_revs.difference_update(queried_revs)
105
first_loop_done = True
107
for revision, parents in result.items():
108
lines.append(' '.join((revision, ) + tuple(parents)))
110
return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
113
class SmartServerRepositoryGetRevisionGraph(SmartServerRepositoryRequest):
115
def do_repository_request(self, repository, revision_id):
116
"""Return the result of repository.get_revision_graph(revision_id).
118
:param repository: The repository to query in.
119
:param revision_id: The utf8 encoded revision_id to get a graph from.
120
:return: A smart server response where the body contains an utf8
121
encoded flattened list of the revision graph.
128
revision_graph = repository.get_revision_graph(revision_id)
129
except errors.NoSuchRevision:
130
# Note that we return an empty body, rather than omitting the body.
131
# This way the client knows that it can always expect to find a body
132
# in the response for this method, even in the error case.
133
return FailedSmartServerResponse(('nosuchrevision', revision_id), '')
135
for revision, parents in revision_graph.items():
136
lines.append(' '.join((revision, ) + tuple(parents)))
138
return SuccessfulSmartServerResponse(('ok', ), '\n'.join(lines))
141
class SmartServerRequestHasRevision(SmartServerRepositoryRequest):
143
def do_repository_request(self, repository, revision_id):
144
"""Return ok if a specific revision is in the repository at path.
146
:param repository: The repository to query in.
147
:param revision_id: The utf8 encoded revision_id to lookup.
148
:return: A smart server response of ('ok', ) if the revision is
151
if repository.has_revision(revision_id):
152
return SuccessfulSmartServerResponse(('yes', ))
154
return SuccessfulSmartServerResponse(('no', ))
157
class SmartServerRepositoryGatherStats(SmartServerRepositoryRequest):
159
def do_repository_request(self, repository, revid, committers):
160
"""Return the result of repository.gather_stats().
162
:param repository: The repository to query in.
163
:param revid: utf8 encoded rev id or an empty string to indicate None
164
:param committers: 'yes' or 'no'.
166
:return: A SmartServerResponse ('ok',), a encoded body looking like
169
latestrev: 345.700 3600
173
But containing only fields returned by the gather_stats() call
176
decoded_revision_id = None
178
decoded_revision_id = revid
179
if committers == 'yes':
180
decoded_committers = True
182
decoded_committers = None
183
stats = repository.gather_stats(decoded_revision_id, decoded_committers)
186
if stats.has_key('committers'):
187
body += 'committers: %d\n' % stats['committers']
188
if stats.has_key('firstrev'):
189
body += 'firstrev: %.3f %d\n' % stats['firstrev']
190
if stats.has_key('latestrev'):
191
body += 'latestrev: %.3f %d\n' % stats['latestrev']
192
if stats.has_key('revisions'):
193
body += 'revisions: %d\n' % stats['revisions']
194
if stats.has_key('size'):
195
body += 'size: %d\n' % stats['size']
197
return SuccessfulSmartServerResponse(('ok', ), body)
200
class SmartServerRepositoryIsShared(SmartServerRepositoryRequest):
202
def do_repository_request(self, repository):
203
"""Return the result of repository.is_shared().
205
:param repository: The repository to query in.
206
:return: A smart server response of ('yes', ) if the repository is
207
shared, and ('no', ) if it is not.
209
if repository.is_shared():
210
return SuccessfulSmartServerResponse(('yes', ))
212
return SuccessfulSmartServerResponse(('no', ))
215
class SmartServerRepositoryLockWrite(SmartServerRepositoryRequest):
217
def do_repository_request(self, repository, token=''):
218
# XXX: this probably should not have a token.
222
token = repository.lock_write(token=token)
223
except errors.LockContention, e:
224
return FailedSmartServerResponse(('LockContention',))
225
except errors.UnlockableTransport:
226
return FailedSmartServerResponse(('UnlockableTransport',))
227
except errors.LockFailed, e:
228
return FailedSmartServerResponse(('LockFailed',
229
str(e.lock), str(e.why)))
230
if token is not None:
231
repository.leave_lock_in_place()
235
return SuccessfulSmartServerResponse(('ok', token))
238
class SmartServerRepositoryUnlock(SmartServerRepositoryRequest):
240
def do_repository_request(self, repository, token):
242
repository.lock_write(token=token)
243
except errors.TokenMismatch, e:
244
return FailedSmartServerResponse(('TokenMismatch',))
245
repository.dont_leave_lock_in_place()
247
return SuccessfulSmartServerResponse(('ok',))
250
class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
251
"""Get the raw repository files as a tarball.
253
The returned tarball contains a .bzr control directory which in turn
254
contains a repository.
256
This takes one parameter, compression, which currently must be
259
This is used to implement the Repository.copy_content_into operation.
262
def do_repository_request(self, repository, compression):
263
from bzrlib import osutils
264
repo_transport = repository.control_files._transport
265
tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
267
controldir_name = tmp_dirname + '/.bzr'
268
return self._tarfile_response(controldir_name, compression)
270
osutils.rmtree(tmp_dirname)
272
def _copy_to_tempdir(self, from_repo):
273
tmp_dirname = tempfile.mkdtemp(prefix='tmpbzrclone')
274
tmp_bzrdir = from_repo.bzrdir._format.initialize(tmp_dirname)
275
tmp_repo = from_repo._format.initialize(tmp_bzrdir)
276
from_repo.copy_content_into(tmp_repo)
277
return tmp_dirname, tmp_repo
279
def _tarfile_response(self, tmp_dirname, compression):
280
temp = tempfile.NamedTemporaryFile()
282
self._tarball_of_dir(tmp_dirname, compression, temp.file)
283
# all finished; write the tempfile out to the network
285
return SuccessfulSmartServerResponse(('ok',), temp.read())
286
# FIXME: Don't read the whole thing into memory here; rather stream it
287
# out from the file onto the network. mbp 20070411
291
def _tarball_of_dir(self, dirname, compression, ofile):
292
filename = os.path.basename(ofile.name)
293
tarball = tarfile.open(fileobj=ofile, name=filename,
294
mode='w|' + compression)
296
# The tarball module only accepts ascii names, and (i guess)
297
# packs them with their 8bit names. We know all the files
298
# within the repository have ASCII names so the should be safe
300
dirname = dirname.encode(sys.getfilesystemencoding())
301
# python's tarball module includes the whole path by default so
303
assert dirname.endswith('.bzr')
304
tarball.add(dirname, '.bzr') # recursive by default
309
class SmartServerRepositoryStreamKnitDataForRevisions(SmartServerRepositoryRequest):
311
def do_repository_request(self, repository, *revision_ids):
312
repository.lock_read()
314
return self._do_repository_request(repository, revision_ids)
318
def _do_repository_request(self, repository, revision_ids):
319
stream = repository.get_data_stream_for_search(
320
repository.revision_ids_to_search_result(set(revision_ids)))
322
pack = ContainerSerialiser()
323
buffer.write(pack.begin())
325
for name_tuple, bytes in stream:
326
buffer.write(pack.bytes_record(bytes, [name_tuple]))
327
except errors.RevisionNotPresent, e:
328
return FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
329
buffer.write(pack.end())
330
return SuccessfulSmartServerResponse(('ok',), buffer.getvalue())
333
class SmartServerRepositoryStreamRevisionsChunked(SmartServerRepositoryRequest):
335
def do_repository_request(self, repository, *revision_ids):
336
repository.lock_read()
338
stream = repository.get_data_stream_for_search(
339
repository.revision_ids_to_search_result(set(revision_ids)))
343
return SuccessfulSmartServerResponse(('ok',),
344
body_stream=self.body_stream(stream, repository))
346
def body_stream(self, stream, repository):
347
pack = ContainerSerialiser()
350
for name_tuple, bytes in stream:
351
yield pack.bytes_record(bytes, [name_tuple])
352
except errors.RevisionNotPresent, e:
353
yield FailedSmartServerResponse(('NoSuchRevision', e.revision_id))