1# Copyright (C) 2006-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Server-side branch related request implmentations."""
18
19from ... import (
20    bencode,
21    errors,
22    revision as _mod_revision,
23    )
24from ...controldir import ControlDir
25from .request import (
26    FailedSmartServerResponse,
27    SmartServerRequest,
28    SuccessfulSmartServerResponse,
29    )
30
31
32class SmartServerBranchRequest(SmartServerRequest):
33    """Base class for handling common branch request logic.
34    """
35
36    def do(self, path, *args):
37        """Execute a request for a branch at path.
38
39        All Branch requests take a path to the branch as their first argument.
40
41        If the branch is a branch reference, NotBranchError is raised.
42
43        :param path: The path for the repository as received from the
44            client.
45        :return: A SmartServerResponse from self.do_with_branch().
46        """
47        transport = self.transport_from_client_path(path)
48        controldir = ControlDir.open_from_transport(transport)
49        if controldir.get_branch_reference() is not None:
50            raise errors.NotBranchError(transport.base)
51        branch = controldir.open_branch(ignore_fallbacks=True)
52        return self.do_with_branch(branch, *args)
53
54
55class SmartServerLockedBranchRequest(SmartServerBranchRequest):
56    """Base class for handling common branch request logic for requests that
57    need a write lock.
58    """
59
60    def do_with_branch(self, branch, branch_token, repo_token, *args):
61        """Execute a request for a branch.
62
63        A write lock will be acquired with the given tokens for the branch and
64        repository locks.  The lock will be released once the request is
65        processed.  The physical lock state won't be changed.
66        """
67        # XXX: write a test for LockContention
68        with branch.repository.lock_write(token=repo_token), \
69                branch.lock_write(token=branch_token):
70            return self.do_with_locked_branch(branch, *args)
71
72
73class SmartServerBranchBreakLock(SmartServerBranchRequest):
74
75    def do_with_branch(self, branch):
76        """Break a branch lock.
77        """
78        branch.break_lock()
79        return SuccessfulSmartServerResponse((b'ok', ), )
80
81
82class SmartServerBranchGetConfigFile(SmartServerBranchRequest):
83
84    def do_with_branch(self, branch):
85        """Return the content of branch.conf
86
87        The body is not utf8 decoded - its the literal bytestream from disk.
88        """
89        try:
90            content = branch.control_transport.get_bytes('branch.conf')
91        except errors.NoSuchFile:
92            content = b''
93        return SuccessfulSmartServerResponse((b'ok', ), content)
94
95
96class SmartServerBranchPutConfigFile(SmartServerBranchRequest):
97    """Set the configuration data for a branch.
98
99    New in 2.5.
100    """
101
102    def do_with_branch(self, branch, branch_token, repo_token):
103        """Set the content of branch.conf.
104
105        The body is not utf8 decoded - its the literal bytestream for disk.
106        """
107        self._branch = branch
108        self._branch_token = branch_token
109        self._repo_token = repo_token
110        # Signal we want a body
111        return None
112
113    def do_body(self, body_bytes):
114        with self._branch.repository.lock_write(token=self._repo_token), \
115                self._branch.lock_write(token=self._branch_token):
116            self._branch.control_transport.put_bytes(
117                'branch.conf', body_bytes)
118        return SuccessfulSmartServerResponse((b'ok', ))
119
120
121class SmartServerBranchGetParent(SmartServerBranchRequest):
122
123    def do_with_branch(self, branch):
124        """Return the parent of branch."""
125        parent = branch._get_parent_location() or ''
126        return SuccessfulSmartServerResponse((parent.encode('utf-8'),))
127
128
129class SmartServerBranchGetTagsBytes(SmartServerBranchRequest):
130
131    def do_with_branch(self, branch):
132        """Return the _get_tags_bytes for a branch."""
133        bytes = branch._get_tags_bytes()
134        return SuccessfulSmartServerResponse((bytes,))
135
136
137class SmartServerBranchSetTagsBytes(SmartServerLockedBranchRequest):
138
139    def __init__(self, backing_transport, root_client_path='/', jail_root=None):
140        SmartServerLockedBranchRequest.__init__(
141            self, backing_transport, root_client_path, jail_root)
142        self.locked = False
143
144    def do_with_locked_branch(self, branch):
145        """Call _set_tags_bytes for a branch.
146
147        New in 1.18.
148        """
149        # We need to keep this branch locked until we get a body with the tags
150        # bytes.
151        self.branch = branch
152        self.branch.lock_write()
153        self.locked = True
154
155    def do_body(self, bytes):
156        self.branch._set_tags_bytes(bytes)
157        return SuccessfulSmartServerResponse(())
158
159    def do_end(self):
160        # TODO: this request shouldn't have to do this housekeeping manually.
161        # Some of this logic probably belongs in a base class.
162        if not self.locked:
163            # We never acquired the branch successfully in the first place, so
164            # there's nothing more to do.
165            return
166        try:
167            return SmartServerLockedBranchRequest.do_end(self)
168        finally:
169            # Only try unlocking if we locked successfully in the first place
170            self.branch.unlock()
171
172
173class SmartServerBranchHeadsToFetch(SmartServerBranchRequest):
174
175    def do_with_branch(self, branch):
176        """Return the heads-to-fetch for a Branch as two bencoded lists.
177
178        See Branch.heads_to_fetch.
179
180        New in 2.4.
181        """
182        must_fetch, if_present_fetch = branch.heads_to_fetch()
183        return SuccessfulSmartServerResponse(
184            (list(must_fetch), list(if_present_fetch)))
185
186
187class SmartServerBranchRequestGetStackedOnURL(SmartServerBranchRequest):
188
189    def do_with_branch(self, branch):
190        stacked_on_url = branch.get_stacked_on_url()
191        return SuccessfulSmartServerResponse((b'ok', stacked_on_url.encode('ascii')))
192
193
194class SmartServerRequestRevisionHistory(SmartServerBranchRequest):
195
196    def do_with_branch(self, branch):
197        """Get the revision history for the branch.
198
199        The revision list is returned as the body content,
200        with each revision utf8 encoded and \x00 joined.
201        """
202        with branch.lock_read():
203            graph = branch.repository.get_graph()
204            stop_revisions = (None, _mod_revision.NULL_REVISION)
205            history = list(graph.iter_lefthand_ancestry(
206                branch.last_revision(), stop_revisions))
207        return SuccessfulSmartServerResponse(
208            (b'ok', ), (b'\x00'.join(reversed(history))))
209
210
211class SmartServerBranchRequestLastRevisionInfo(SmartServerBranchRequest):
212
213    def do_with_branch(self, branch):
214        """Return branch.last_revision_info().
215
216        The revno is encoded in decimal, the revision_id is encoded as utf8.
217        """
218        revno, last_revision = branch.last_revision_info()
219        return SuccessfulSmartServerResponse(
220            (b'ok', str(revno).encode('ascii'), last_revision))
221
222
223class SmartServerBranchRequestRevisionIdToRevno(SmartServerBranchRequest):
224
225    def do_with_branch(self, branch, revid):
226        """Return branch.revision_id_to_revno().
227
228        New in 2.5.
229
230        The revno is encoded in decimal, the revision_id is encoded as utf8.
231        """
232        try:
233            dotted_revno = branch.revision_id_to_dotted_revno(revid)
234        except errors.NoSuchRevision:
235            return FailedSmartServerResponse((b'NoSuchRevision', revid))
236        except errors.GhostRevisionsHaveNoRevno as e:
237            return FailedSmartServerResponse(
238                (b'GhostRevisionsHaveNoRevno', e.revision_id,
239                    e.ghost_revision_id))
240        return SuccessfulSmartServerResponse(
241            (b'ok', ) + tuple([b'%d' % x for x in dotted_revno]))
242
243
244class SmartServerSetTipRequest(SmartServerLockedBranchRequest):
245    """Base class for handling common branch request logic for requests that
246    update the branch tip.
247    """
248
249    def do_with_locked_branch(self, branch, *args):
250        try:
251            return self.do_tip_change_with_locked_branch(branch, *args)
252        except errors.TipChangeRejected as e:
253            msg = e.msg
254            if isinstance(msg, str):
255                msg = msg.encode('utf-8')
256            return FailedSmartServerResponse((b'TipChangeRejected', msg))
257
258
259class SmartServerBranchRequestSetConfigOption(SmartServerLockedBranchRequest):
260    """Set an option in the branch configuration."""
261
262    def do_with_locked_branch(self, branch, value, name, section):
263        if not section:
264            section = None
265        branch._get_config().set_option(
266            value.decode('utf-8'), name.decode('utf-8'),
267            section.decode('utf-8') if section is not None else None)
268        return SuccessfulSmartServerResponse(())
269
270
271class SmartServerBranchRequestSetConfigOptionDict(SmartServerLockedBranchRequest):
272    """Set an option in the branch configuration.
273
274    New in 2.2.
275    """
276
277    def do_with_locked_branch(self, branch, value_dict, name, section):
278        utf8_dict = bencode.bdecode(value_dict)
279        value_dict = {}
280        for key, value in utf8_dict.items():
281            value_dict[key.decode('utf8')] = value.decode('utf8')
282        if not section:
283            section = None
284        else:
285            section = section.decode('utf-8')
286        branch._get_config().set_option(value_dict, name.decode('utf-8'), section)
287        return SuccessfulSmartServerResponse(())
288
289
290class SmartServerBranchRequestSetLastRevision(SmartServerSetTipRequest):
291
292    def do_tip_change_with_locked_branch(self, branch, new_last_revision_id):
293        if new_last_revision_id == b'null:':
294            branch.set_last_revision_info(0, new_last_revision_id)
295        else:
296            if not branch.repository.has_revision(new_last_revision_id):
297                return FailedSmartServerResponse(
298                    (b'NoSuchRevision', new_last_revision_id))
299            branch.generate_revision_history(new_last_revision_id, None, None)
300        return SuccessfulSmartServerResponse((b'ok',))
301
302
303class SmartServerBranchRequestSetLastRevisionEx(SmartServerSetTipRequest):
304
305    def do_tip_change_with_locked_branch(self, branch, new_last_revision_id,
306                                         allow_divergence, allow_overwrite_descendant):
307        """Set the last revision of the branch.
308
309        New in 1.6.
310
311        :param new_last_revision_id: the revision ID to set as the last
312            revision of the branch.
313        :param allow_divergence: A flag.  If non-zero, change the revision ID
314            even if the new_last_revision_id's ancestry has diverged from the
315            current last revision.  If zero, a 'Diverged' error will be
316            returned if new_last_revision_id is not a descendant of the current
317            last revision.
318        :param allow_overwrite_descendant:  A flag.  If zero and
319            new_last_revision_id is not a descendant of the current last
320            revision, then the last revision will not be changed.  If non-zero
321            and there is no divergence, then the last revision is always
322            changed.
323
324        :returns: on success, a tuple of ('ok', revno, revision_id), where
325            revno and revision_id are the new values of the current last
326            revision info.  The revision_id might be different to the
327            new_last_revision_id if allow_overwrite_descendant was not set.
328        """
329        do_not_overwrite_descendant = not allow_overwrite_descendant
330        try:
331            last_revno, last_rev = branch.last_revision_info()
332            graph = branch.repository.get_graph()
333            if not allow_divergence or do_not_overwrite_descendant:
334                relation = branch._revision_relations(
335                    last_rev, new_last_revision_id, graph)
336                if relation == 'diverged' and not allow_divergence:
337                    return FailedSmartServerResponse((b'Diverged',))
338                if relation == 'a_descends_from_b' and do_not_overwrite_descendant:
339                    return SuccessfulSmartServerResponse(
340                        (b'ok', last_revno, last_rev))
341            new_revno = graph.find_distance_to_null(
342                new_last_revision_id, [(last_rev, last_revno)])
343            branch.set_last_revision_info(new_revno, new_last_revision_id)
344        except errors.GhostRevisionsHaveNoRevno:
345            return FailedSmartServerResponse(
346                (b'NoSuchRevision', new_last_revision_id))
347        return SuccessfulSmartServerResponse(
348            (b'ok', new_revno, new_last_revision_id))
349
350
351class SmartServerBranchRequestSetLastRevisionInfo(SmartServerSetTipRequest):
352    """Branch.set_last_revision_info.  Sets the revno and the revision ID of
353    the specified branch.
354
355    New in breezy 1.4.
356    """
357
358    def do_tip_change_with_locked_branch(self, branch, new_revno,
359                                         new_last_revision_id):
360        try:
361            branch.set_last_revision_info(int(new_revno), new_last_revision_id)
362        except errors.NoSuchRevision:
363            return FailedSmartServerResponse(
364                (b'NoSuchRevision', new_last_revision_id))
365        return SuccessfulSmartServerResponse((b'ok',))
366
367
368class SmartServerBranchRequestSetParentLocation(SmartServerLockedBranchRequest):
369    """Set the parent location for a branch.
370
371    Takes a location to set, which must be utf8 encoded.
372    """
373
374    def do_with_locked_branch(self, branch, location):
375        branch._set_parent_location(location.decode('utf-8'))
376        return SuccessfulSmartServerResponse(())
377
378
379class SmartServerBranchRequestLockWrite(SmartServerBranchRequest):
380
381    def do_with_branch(self, branch, branch_token=b'', repo_token=b''):
382        if branch_token == b'':
383            branch_token = None
384        if repo_token == b'':
385            repo_token = None
386        try:
387            repo_token = branch.repository.lock_write(
388                token=repo_token).repository_token
389            try:
390                branch_token = branch.lock_write(
391                    token=branch_token).token
392            finally:
393                # this leaves the repository with 1 lock
394                branch.repository.unlock()
395        except errors.LockContention:
396            return FailedSmartServerResponse((b'LockContention',))
397        except errors.TokenMismatch:
398            return FailedSmartServerResponse((b'TokenMismatch',))
399        except errors.UnlockableTransport:
400            return FailedSmartServerResponse((b'UnlockableTransport',))
401        except errors.LockFailed as e:
402            return FailedSmartServerResponse((b'LockFailed',
403                                              str(e.lock).encode('utf-8'), str(e.why).encode('utf-8')))
404        if repo_token is None:
405            repo_token = b''
406        else:
407            branch.repository.leave_lock_in_place()
408        branch.leave_lock_in_place()
409        branch.unlock()
410        return SuccessfulSmartServerResponse((b'ok', branch_token, repo_token))
411
412
413class SmartServerBranchRequestUnlock(SmartServerBranchRequest):
414
415    def do_with_branch(self, branch, branch_token, repo_token):
416        try:
417            with branch.repository.lock_write(token=repo_token):
418                branch.lock_write(token=branch_token)
419        except errors.TokenMismatch:
420            return FailedSmartServerResponse((b'TokenMismatch',))
421        if repo_token:
422            branch.repository.dont_leave_lock_in_place()
423        branch.dont_leave_lock_in_place()
424        branch.unlock()
425        return SuccessfulSmartServerResponse((b'ok',))
426
427
428class SmartServerBranchRequestGetPhysicalLockStatus(SmartServerBranchRequest):
429    """Get the physical lock status for a branch.
430
431    New in 2.5.
432    """
433
434    def do_with_branch(self, branch):
435        if branch.get_physical_lock_status():
436            return SuccessfulSmartServerResponse((b'yes',))
437        else:
438            return SuccessfulSmartServerResponse((b'no',))
439
440
441class SmartServerBranchRequestGetAllReferenceInfo(SmartServerBranchRequest):
442    """Get the reference information.
443
444    New in 3.1.
445    """
446
447    def do_with_branch(self, branch):
448        all_reference_info = branch._get_all_reference_info()
449        content = bencode.bencode([
450            (key, value[0].encode('utf-8'), value[1].encode('utf-8') if value[1] else b'')
451            for (key, value) in all_reference_info.items()])
452        return SuccessfulSmartServerResponse((b'ok', ), content)
453