1
# Copyright (C) 2008 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 versioned files related request implmentations."""
20
from cStringIO import StringIO
26
from bzrlib.bzrdir import BzrDir
27
from bzrlib.smart.request import (
28
FailedSmartServerResponse,
30
SuccessfulSmartServerResponse,
32
from bzrlib.smart.repository import SmartServerRepositoryRequest
33
from bzrlib.repository import _strip_NULL_ghosts
34
from bzrlib import revision as _mod_revision
37
class SmartServerVersionedFilesRequest(SmartServerRepositoryRequest):
38
"""Common base class for Repository requests."""
40
def do(self, path, *args):
41
"""Execute a repository request.
43
All Repository requests take a path to the repository as their first
44
argument. The repository must be at the exact path given by the
45
client - no searching is done.
47
The actual logic is delegated to self.do_repository_request.
49
:param client_path: The path for the repository as received from the
51
:return: A SmartServerResponse from self.do_repository_request().
53
transport = self.transport_from_client_path(path)
54
bzrdir = BzrDir.open_from_transport(transport)
55
# Save the repository for use with do_body.
56
self._repository = bzrdir.open_repository()
57
return self.do_repository_request(self._repository, *args)
59
def do_repository_request(self, repository, vf_name, *args):
60
if vf_name not in ['texts', 'inventories', 'signatures', 'revisions']:
61
return FailedSmartServerResponse(('NoSuchVersionedFile', vf_name))
62
versioned_files = getattr(repository, vf_name)
63
return self.do_versioned_files_request(versioned_files, *args)
65
def do_versioned_files_request(self, versioned_files, *args):
66
"""Override to provide an implementation for a verb."""
69
class SmartServerVersionedFilesGetParentMap(SmartServerVersionedFilesRequest):
70
"""Bzr 1.2+ - get parent data for revisions during a graph search."""
72
def do_versioned_files_request(self, versioned_files, *keys):
73
"""Get parent details for some revisions.
75
All the parents for revision_ids are returned. Additionally up to 64KB
76
of additional parent data found by performing a breadth first search
77
from revision_ids is returned. The verb takes a body containing the
78
current search state, see do_body for details.
80
:param repository: The repository to query in.
81
:param revision_ids: The utf8 encoded revision_id to answer for.
83
self._keys = tuple(tuple(key) for key in keys)
84
self._versioned_files = versioned_files
85
return None # Signal that we want a body.
87
def do_body(self, body_bytes):
88
"""Process the current search state and perform the parent lookup.
90
:return: A smart server response where the body contains an utf8
91
encoded flattened list of the parents of the keys which has been
94
repository = self._repository
95
repository.lock_read()
97
vf_graph = graph.Graph(self._versioned_files)
98
return self._do_repository_request(body_bytes, vf_graph)
102
def _deserialise_search_tuple_key_recipe(self, bytes):
103
start_keys_bytes, stop_keys_bytes, count_bytes = bytes.split('\n')
104
start_keys = [tuple(k.split(' ')) for k in start_keys_bytes.split('\0')]
105
stop_keys = [tuple(k.split(' ')) for k in stop_keys_bytes.split('\0')]
106
count = int(count_bytes)
107
return tuple(start_keys), set(stop_keys), count
109
def recreate_vf_search(self, vf_graph, recipe_bytes):
110
recipe = self._deserialise_search_tuple_key_recipe(recipe_bytes)
111
start_keys, exclude_keys, key_count = recipe
114
search = vf_graph._make_breadth_first_searcher(start_keys)
117
next_revs = search.next()
118
except StopIteration:
120
search.stop_searching_any(exclude_keys.intersection(next_revs))
121
search_result = search.get_result()
122
if search_result.get_recipe()[2] != key_count:
123
# we got back a different amount of data than expected, this
124
# gets reported as NoSuchRevision, because less revisions
125
# indicates missing revisions, and more should never happen as
126
# the excludes list considers ghosts and ensures that ghost
127
# filling races are not a problem.
128
return (None, FailedSmartServerResponse(('NoSuchRevision',)))
129
return (search, None)
134
def _do_repository_request(self, body_bytes, vf_graph):
135
repository = self._repository
136
keys = set(self._keys)
137
search, error = self.recreate_vf_search(vf_graph, body_bytes)
138
if error is not None:
140
# TODO might be nice to start up the search again; but thats not
141
# written or tested yet.
142
client_seen_keys = set(search.get_result().get_keys())
143
# Always include the requested ids.
144
client_seen_keys.difference_update(keys)
150
first_loop_done = False
152
queried_keys.update(next_keys)
153
parent_map = vf_graph.get_parent_map(next_keys)
155
for key, parents in parent_map.iteritems():
156
# prepare the next query
157
next_keys.update(parents)
158
if key not in client_seen_keys:
159
# Client does not have this revision, give it to it.
160
# add parents to the result
161
result[key] = parents
162
# Approximate the serialized cost of this key.
163
# XXX: this approximation is out of date
164
size_so_far += 2 + len(key) + sum(map(len, parents))
165
# get all the directly asked for parents, and then flesh out to
166
# 64K (compressed) or so. We do one level of depth at a time to
167
# stay in sync with the client. The 250000 magic number is
168
# estimated compression ratio taken from bzr.dev itself.
169
if first_loop_done and size_so_far > 250000:
172
# don't query things we've already queried
173
next_keys.difference_update(queried_keys)
174
first_loop_done = True
176
# sorting trivially puts lexographically similar revision ids together.
178
result = sorted(result.items())
180
return SuccessfulSmartServerResponse(
181
('ok', ), bz2.compress(_serialise_search_result(result)))
184
def _serialise_search_result(result_items):
187
for key, parents in result_items:
192
buf.write(' '.join(key))
194
parents_iter = (' '.join(parent) for parent in parents)
195
buf.write('\0'.join(parents_iter))
196
return buf.getvalue()