##// END OF EJS Templates
wireprotov2: add TODOs around extending changesetdata fields...
Gregory Szorc -
r39672:399ddd32 default
parent child Browse files
Show More
@@ -1,178 +1,181
1 # exchangev2.py - repository exchange for wire protocol version 2
1 # exchangev2.py - repository exchange for wire protocol version 2
2 #
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import weakref
10 import weakref
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 nullid,
14 nullid,
15 short,
15 short,
16 )
16 )
17 from . import (
17 from . import (
18 bookmarks,
18 bookmarks,
19 mdiff,
19 mdiff,
20 phases,
20 phases,
21 pycompat,
21 pycompat,
22 setdiscovery,
22 setdiscovery,
23 )
23 )
24
24
25 def pull(pullop):
25 def pull(pullop):
26 """Pull using wire protocol version 2."""
26 """Pull using wire protocol version 2."""
27 repo = pullop.repo
27 repo = pullop.repo
28 remote = pullop.remote
28 remote = pullop.remote
29 tr = pullop.trmanager.transaction()
29 tr = pullop.trmanager.transaction()
30
30
31 # Figure out what needs to be fetched.
31 # Figure out what needs to be fetched.
32 common, fetch, remoteheads = _pullchangesetdiscovery(
32 common, fetch, remoteheads = _pullchangesetdiscovery(
33 repo, remote, pullop.heads, abortwhenunrelated=pullop.force)
33 repo, remote, pullop.heads, abortwhenunrelated=pullop.force)
34
34
35 # And fetch the data.
35 # And fetch the data.
36 pullheads = pullop.heads or remoteheads
36 pullheads = pullop.heads or remoteheads
37 csetres = _fetchchangesets(repo, tr, remote, common, fetch, pullheads)
37 csetres = _fetchchangesets(repo, tr, remote, common, fetch, pullheads)
38
38
39 # New revisions are written to the changelog. But all other updates
39 # New revisions are written to the changelog. But all other updates
40 # are deferred. Do those now.
40 # are deferred. Do those now.
41
41
42 # Ensure all new changesets are draft by default. If the repo is
42 # Ensure all new changesets are draft by default. If the repo is
43 # publishing, the phase will be adjusted by the loop below.
43 # publishing, the phase will be adjusted by the loop below.
44 if csetres['added']:
44 if csetres['added']:
45 phases.registernew(repo, tr, phases.draft, csetres['added'])
45 phases.registernew(repo, tr, phases.draft, csetres['added'])
46
46
47 # And adjust the phase of all changesets accordingly.
47 # And adjust the phase of all changesets accordingly.
48 for phase in phases.phasenames:
48 for phase in phases.phasenames:
49 if phase == b'secret' or not csetres['nodesbyphase'][phase]:
49 if phase == b'secret' or not csetres['nodesbyphase'][phase]:
50 continue
50 continue
51
51
52 phases.advanceboundary(repo, tr, phases.phasenames.index(phase),
52 phases.advanceboundary(repo, tr, phases.phasenames.index(phase),
53 csetres['nodesbyphase'][phase])
53 csetres['nodesbyphase'][phase])
54
54
55 # Write bookmark updates.
55 # Write bookmark updates.
56 bookmarks.updatefromremote(repo.ui, repo, csetres['bookmarks'],
56 bookmarks.updatefromremote(repo.ui, repo, csetres['bookmarks'],
57 remote.url(), pullop.gettransaction,
57 remote.url(), pullop.gettransaction,
58 explicit=pullop.explicitbookmarks)
58 explicit=pullop.explicitbookmarks)
59
59
60 def _pullchangesetdiscovery(repo, remote, heads, abortwhenunrelated=True):
60 def _pullchangesetdiscovery(repo, remote, heads, abortwhenunrelated=True):
61 """Determine which changesets need to be pulled."""
61 """Determine which changesets need to be pulled."""
62
62
63 if heads:
63 if heads:
64 knownnode = repo.changelog.hasnode
64 knownnode = repo.changelog.hasnode
65 if all(knownnode(head) for head in heads):
65 if all(knownnode(head) for head in heads):
66 return heads, False, heads
66 return heads, False, heads
67
67
68 # TODO wire protocol version 2 is capable of more efficient discovery
68 # TODO wire protocol version 2 is capable of more efficient discovery
69 # than setdiscovery. Consider implementing something better.
69 # than setdiscovery. Consider implementing something better.
70 common, fetch, remoteheads = setdiscovery.findcommonheads(
70 common, fetch, remoteheads = setdiscovery.findcommonheads(
71 repo.ui, repo, remote, abortwhenunrelated=abortwhenunrelated)
71 repo.ui, repo, remote, abortwhenunrelated=abortwhenunrelated)
72
72
73 common = set(common)
73 common = set(common)
74 remoteheads = set(remoteheads)
74 remoteheads = set(remoteheads)
75
75
76 # If a remote head is filtered locally, put it back in the common set.
76 # If a remote head is filtered locally, put it back in the common set.
77 # See the comment in exchange._pulldiscoverychangegroup() for more.
77 # See the comment in exchange._pulldiscoverychangegroup() for more.
78
78
79 if fetch and remoteheads:
79 if fetch and remoteheads:
80 nodemap = repo.unfiltered().changelog.nodemap
80 nodemap = repo.unfiltered().changelog.nodemap
81
81
82 common |= {head for head in remoteheads if head in nodemap}
82 common |= {head for head in remoteheads if head in nodemap}
83
83
84 if set(remoteheads).issubset(common):
84 if set(remoteheads).issubset(common):
85 fetch = []
85 fetch = []
86
86
87 common.discard(nullid)
87 common.discard(nullid)
88
88
89 return common, fetch, remoteheads
89 return common, fetch, remoteheads
90
90
91 def _fetchchangesets(repo, tr, remote, common, fetch, remoteheads):
91 def _fetchchangesets(repo, tr, remote, common, fetch, remoteheads):
92 # TODO consider adding a step here where we obtain the DAG shape first
92 # TODO consider adding a step here where we obtain the DAG shape first
93 # (or ask the server to slice changesets into chunks for us) so that
93 # (or ask the server to slice changesets into chunks for us) so that
94 # we can perform multiple fetches in batches. This will facilitate
94 # we can perform multiple fetches in batches. This will facilitate
95 # resuming interrupted clones, higher server-side cache hit rates due
95 # resuming interrupted clones, higher server-side cache hit rates due
96 # to smaller segments, etc.
96 # to smaller segments, etc.
97 with remote.commandexecutor() as e:
97 with remote.commandexecutor() as e:
98 objs = e.callcommand(b'changesetdata', {
98 objs = e.callcommand(b'changesetdata', {
99 b'noderange': [sorted(common), sorted(remoteheads)],
99 b'noderange': [sorted(common), sorted(remoteheads)],
100 b'fields': {b'bookmarks', b'parents', b'phase', b'revision'},
100 b'fields': {b'bookmarks', b'parents', b'phase', b'revision'},
101 }).result()
101 }).result()
102
102
103 # The context manager waits on all response data when exiting. So
103 # The context manager waits on all response data when exiting. So
104 # we need to remain in the context manager in order to stream data.
104 # we need to remain in the context manager in order to stream data.
105 return _processchangesetdata(repo, tr, objs)
105 return _processchangesetdata(repo, tr, objs)
106
106
107 def _processchangesetdata(repo, tr, objs):
107 def _processchangesetdata(repo, tr, objs):
108 repo.hook('prechangegroup', throw=True,
108 repo.hook('prechangegroup', throw=True,
109 **pycompat.strkwargs(tr.hookargs))
109 **pycompat.strkwargs(tr.hookargs))
110
110
111 urepo = repo.unfiltered()
111 urepo = repo.unfiltered()
112 cl = urepo.changelog
112 cl = urepo.changelog
113
113
114 cl.delayupdate(tr)
114 cl.delayupdate(tr)
115
115
116 # The first emitted object is a header describing the data that
116 # The first emitted object is a header describing the data that
117 # follows.
117 # follows.
118 meta = next(objs)
118 meta = next(objs)
119
119
120 progress = repo.ui.makeprogress(_('changesets'),
120 progress = repo.ui.makeprogress(_('changesets'),
121 unit=_('chunks'),
121 unit=_('chunks'),
122 total=meta.get(b'totalitems'))
122 total=meta.get(b'totalitems'))
123
123
124 def linkrev(node):
124 def linkrev(node):
125 repo.ui.debug('add changeset %s\n' % short(node))
125 repo.ui.debug('add changeset %s\n' % short(node))
126 # Linkrev for changelog is always self.
126 # Linkrev for changelog is always self.
127 return len(cl)
127 return len(cl)
128
128
129 def onchangeset(cl, node):
129 def onchangeset(cl, node):
130 progress.increment()
130 progress.increment()
131
131
132 nodesbyphase = {phase: set() for phase in phases.phasenames}
132 nodesbyphase = {phase: set() for phase in phases.phasenames}
133 remotebookmarks = {}
133 remotebookmarks = {}
134
134
135 # addgroup() expects a 7-tuple describing revisions. This normalizes
135 # addgroup() expects a 7-tuple describing revisions. This normalizes
136 # the wire data to that format.
136 # the wire data to that format.
137 #
137 #
138 # This loop also aggregates non-revision metadata, such as phase
138 # This loop also aggregates non-revision metadata, such as phase
139 # data.
139 # data.
140 def iterrevisions():
140 def iterrevisions():
141 for cset in objs:
141 for cset in objs:
142 node = cset[b'node']
142 node = cset[b'node']
143
143
144 if b'phase' in cset:
144 if b'phase' in cset:
145 nodesbyphase[cset[b'phase']].add(node)
145 nodesbyphase[cset[b'phase']].add(node)
146
146
147 for mark in cset.get(b'bookmarks', []):
147 for mark in cset.get(b'bookmarks', []):
148 remotebookmarks[mark] = node
148 remotebookmarks[mark] = node
149
149
150 # TODO add mechanism for extensions to examine records so they
151 # can siphon off custom data fields.
152
150 # Some entries might only be metadata only updates.
153 # Some entries might only be metadata only updates.
151 if b'revisionsize' not in cset:
154 if b'revisionsize' not in cset:
152 continue
155 continue
153
156
154 data = next(objs)
157 data = next(objs)
155
158
156 yield (
159 yield (
157 node,
160 node,
158 cset[b'parents'][0],
161 cset[b'parents'][0],
159 cset[b'parents'][1],
162 cset[b'parents'][1],
160 # Linknode is always itself for changesets.
163 # Linknode is always itself for changesets.
161 cset[b'node'],
164 cset[b'node'],
162 # We always send full revisions. So delta base is not set.
165 # We always send full revisions. So delta base is not set.
163 nullid,
166 nullid,
164 mdiff.trivialdiffheader(len(data)) + data,
167 mdiff.trivialdiffheader(len(data)) + data,
165 # Flags not yet supported.
168 # Flags not yet supported.
166 0,
169 0,
167 )
170 )
168
171
169 added = cl.addgroup(iterrevisions(), linkrev, weakref.proxy(tr),
172 added = cl.addgroup(iterrevisions(), linkrev, weakref.proxy(tr),
170 addrevisioncb=onchangeset)
173 addrevisioncb=onchangeset)
171
174
172 progress.complete()
175 progress.complete()
173
176
174 return {
177 return {
175 'added': added,
178 'added': added,
176 'nodesbyphase': nodesbyphase,
179 'nodesbyphase': nodesbyphase,
177 'bookmarks': remotebookmarks,
180 'bookmarks': remotebookmarks,
178 }
181 }
@@ -1,639 +1,646
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10
10
11 from .i18n import _
11 from .i18n import _
12 from .node import (
12 from .node import (
13 nullid,
13 nullid,
14 )
14 )
15 from . import (
15 from . import (
16 discovery,
16 discovery,
17 encoding,
17 encoding,
18 error,
18 error,
19 pycompat,
19 pycompat,
20 streamclone,
20 streamclone,
21 util,
21 util,
22 wireprotoframing,
22 wireprotoframing,
23 wireprototypes,
23 wireprototypes,
24 )
24 )
25 from .utils import (
25 from .utils import (
26 interfaceutil,
26 interfaceutil,
27 )
27 )
28
28
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
30
30
31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
32
32
33 COMMANDS = wireprototypes.commanddict()
33 COMMANDS = wireprototypes.commanddict()
34
34
35 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
35 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
36 from .hgweb import common as hgwebcommon
36 from .hgweb import common as hgwebcommon
37
37
38 # URL space looks like: <permissions>/<command>, where <permission> can
38 # URL space looks like: <permissions>/<command>, where <permission> can
39 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
39 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
40
40
41 # Root URL does nothing meaningful... yet.
41 # Root URL does nothing meaningful... yet.
42 if not urlparts:
42 if not urlparts:
43 res.status = b'200 OK'
43 res.status = b'200 OK'
44 res.headers[b'Content-Type'] = b'text/plain'
44 res.headers[b'Content-Type'] = b'text/plain'
45 res.setbodybytes(_('HTTP version 2 API handler'))
45 res.setbodybytes(_('HTTP version 2 API handler'))
46 return
46 return
47
47
48 if len(urlparts) == 1:
48 if len(urlparts) == 1:
49 res.status = b'404 Not Found'
49 res.status = b'404 Not Found'
50 res.headers[b'Content-Type'] = b'text/plain'
50 res.headers[b'Content-Type'] = b'text/plain'
51 res.setbodybytes(_('do not know how to process %s\n') %
51 res.setbodybytes(_('do not know how to process %s\n') %
52 req.dispatchpath)
52 req.dispatchpath)
53 return
53 return
54
54
55 permission, command = urlparts[0:2]
55 permission, command = urlparts[0:2]
56
56
57 if permission not in (b'ro', b'rw'):
57 if permission not in (b'ro', b'rw'):
58 res.status = b'404 Not Found'
58 res.status = b'404 Not Found'
59 res.headers[b'Content-Type'] = b'text/plain'
59 res.headers[b'Content-Type'] = b'text/plain'
60 res.setbodybytes(_('unknown permission: %s') % permission)
60 res.setbodybytes(_('unknown permission: %s') % permission)
61 return
61 return
62
62
63 if req.method != 'POST':
63 if req.method != 'POST':
64 res.status = b'405 Method Not Allowed'
64 res.status = b'405 Method Not Allowed'
65 res.headers[b'Allow'] = b'POST'
65 res.headers[b'Allow'] = b'POST'
66 res.setbodybytes(_('commands require POST requests'))
66 res.setbodybytes(_('commands require POST requests'))
67 return
67 return
68
68
69 # At some point we'll want to use our own API instead of recycling the
69 # At some point we'll want to use our own API instead of recycling the
70 # behavior of version 1 of the wire protocol...
70 # behavior of version 1 of the wire protocol...
71 # TODO return reasonable responses - not responses that overload the
71 # TODO return reasonable responses - not responses that overload the
72 # HTTP status line message for error reporting.
72 # HTTP status line message for error reporting.
73 try:
73 try:
74 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
74 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
75 except hgwebcommon.ErrorResponse as e:
75 except hgwebcommon.ErrorResponse as e:
76 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
76 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
77 for k, v in e.headers:
77 for k, v in e.headers:
78 res.headers[k] = v
78 res.headers[k] = v
79 res.setbodybytes('permission denied')
79 res.setbodybytes('permission denied')
80 return
80 return
81
81
82 # We have a special endpoint to reflect the request back at the client.
82 # We have a special endpoint to reflect the request back at the client.
83 if command == b'debugreflect':
83 if command == b'debugreflect':
84 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
84 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
85 return
85 return
86
86
87 # Extra commands that we handle that aren't really wire protocol
87 # Extra commands that we handle that aren't really wire protocol
88 # commands. Think extra hard before making this hackery available to
88 # commands. Think extra hard before making this hackery available to
89 # extension.
89 # extension.
90 extracommands = {'multirequest'}
90 extracommands = {'multirequest'}
91
91
92 if command not in COMMANDS and command not in extracommands:
92 if command not in COMMANDS and command not in extracommands:
93 res.status = b'404 Not Found'
93 res.status = b'404 Not Found'
94 res.headers[b'Content-Type'] = b'text/plain'
94 res.headers[b'Content-Type'] = b'text/plain'
95 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
95 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
96 return
96 return
97
97
98 repo = rctx.repo
98 repo = rctx.repo
99 ui = repo.ui
99 ui = repo.ui
100
100
101 proto = httpv2protocolhandler(req, ui)
101 proto = httpv2protocolhandler(req, ui)
102
102
103 if (not COMMANDS.commandavailable(command, proto)
103 if (not COMMANDS.commandavailable(command, proto)
104 and command not in extracommands):
104 and command not in extracommands):
105 res.status = b'404 Not Found'
105 res.status = b'404 Not Found'
106 res.headers[b'Content-Type'] = b'text/plain'
106 res.headers[b'Content-Type'] = b'text/plain'
107 res.setbodybytes(_('invalid wire protocol command: %s') % command)
107 res.setbodybytes(_('invalid wire protocol command: %s') % command)
108 return
108 return
109
109
110 # TODO consider cases where proxies may add additional Accept headers.
110 # TODO consider cases where proxies may add additional Accept headers.
111 if req.headers.get(b'Accept') != FRAMINGTYPE:
111 if req.headers.get(b'Accept') != FRAMINGTYPE:
112 res.status = b'406 Not Acceptable'
112 res.status = b'406 Not Acceptable'
113 res.headers[b'Content-Type'] = b'text/plain'
113 res.headers[b'Content-Type'] = b'text/plain'
114 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
114 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
115 % FRAMINGTYPE)
115 % FRAMINGTYPE)
116 return
116 return
117
117
118 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
118 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
119 res.status = b'415 Unsupported Media Type'
119 res.status = b'415 Unsupported Media Type'
120 # TODO we should send a response with appropriate media type,
120 # TODO we should send a response with appropriate media type,
121 # since client does Accept it.
121 # since client does Accept it.
122 res.headers[b'Content-Type'] = b'text/plain'
122 res.headers[b'Content-Type'] = b'text/plain'
123 res.setbodybytes(_('client MUST send Content-Type header with '
123 res.setbodybytes(_('client MUST send Content-Type header with '
124 'value: %s\n') % FRAMINGTYPE)
124 'value: %s\n') % FRAMINGTYPE)
125 return
125 return
126
126
127 _processhttpv2request(ui, repo, req, res, permission, command, proto)
127 _processhttpv2request(ui, repo, req, res, permission, command, proto)
128
128
129 def _processhttpv2reflectrequest(ui, repo, req, res):
129 def _processhttpv2reflectrequest(ui, repo, req, res):
130 """Reads unified frame protocol request and dumps out state to client.
130 """Reads unified frame protocol request and dumps out state to client.
131
131
132 This special endpoint can be used to help debug the wire protocol.
132 This special endpoint can be used to help debug the wire protocol.
133
133
134 Instead of routing the request through the normal dispatch mechanism,
134 Instead of routing the request through the normal dispatch mechanism,
135 we instead read all frames, decode them, and feed them into our state
135 we instead read all frames, decode them, and feed them into our state
136 tracker. We then dump the log of all that activity back out to the
136 tracker. We then dump the log of all that activity back out to the
137 client.
137 client.
138 """
138 """
139 import json
139 import json
140
140
141 # Reflection APIs have a history of being abused, accidentally disclosing
141 # Reflection APIs have a history of being abused, accidentally disclosing
142 # sensitive data, etc. So we have a config knob.
142 # sensitive data, etc. So we have a config knob.
143 if not ui.configbool('experimental', 'web.api.debugreflect'):
143 if not ui.configbool('experimental', 'web.api.debugreflect'):
144 res.status = b'404 Not Found'
144 res.status = b'404 Not Found'
145 res.headers[b'Content-Type'] = b'text/plain'
145 res.headers[b'Content-Type'] = b'text/plain'
146 res.setbodybytes(_('debugreflect service not available'))
146 res.setbodybytes(_('debugreflect service not available'))
147 return
147 return
148
148
149 # We assume we have a unified framing protocol request body.
149 # We assume we have a unified framing protocol request body.
150
150
151 reactor = wireprotoframing.serverreactor()
151 reactor = wireprotoframing.serverreactor()
152 states = []
152 states = []
153
153
154 while True:
154 while True:
155 frame = wireprotoframing.readframe(req.bodyfh)
155 frame = wireprotoframing.readframe(req.bodyfh)
156
156
157 if not frame:
157 if not frame:
158 states.append(b'received: <no frame>')
158 states.append(b'received: <no frame>')
159 break
159 break
160
160
161 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
161 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
162 frame.requestid,
162 frame.requestid,
163 frame.payload))
163 frame.payload))
164
164
165 action, meta = reactor.onframerecv(frame)
165 action, meta = reactor.onframerecv(frame)
166 states.append(json.dumps((action, meta), sort_keys=True,
166 states.append(json.dumps((action, meta), sort_keys=True,
167 separators=(', ', ': ')))
167 separators=(', ', ': ')))
168
168
169 action, meta = reactor.oninputeof()
169 action, meta = reactor.oninputeof()
170 meta['action'] = action
170 meta['action'] = action
171 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
171 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
172
172
173 res.status = b'200 OK'
173 res.status = b'200 OK'
174 res.headers[b'Content-Type'] = b'text/plain'
174 res.headers[b'Content-Type'] = b'text/plain'
175 res.setbodybytes(b'\n'.join(states))
175 res.setbodybytes(b'\n'.join(states))
176
176
177 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
177 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
178 """Post-validation handler for HTTPv2 requests.
178 """Post-validation handler for HTTPv2 requests.
179
179
180 Called when the HTTP request contains unified frame-based protocol
180 Called when the HTTP request contains unified frame-based protocol
181 frames for evaluation.
181 frames for evaluation.
182 """
182 """
183 # TODO Some HTTP clients are full duplex and can receive data before
183 # TODO Some HTTP clients are full duplex and can receive data before
184 # the entire request is transmitted. Figure out a way to indicate support
184 # the entire request is transmitted. Figure out a way to indicate support
185 # for that so we can opt into full duplex mode.
185 # for that so we can opt into full duplex mode.
186 reactor = wireprotoframing.serverreactor(deferoutput=True)
186 reactor = wireprotoframing.serverreactor(deferoutput=True)
187 seencommand = False
187 seencommand = False
188
188
189 outstream = reactor.makeoutputstream()
189 outstream = reactor.makeoutputstream()
190
190
191 while True:
191 while True:
192 frame = wireprotoframing.readframe(req.bodyfh)
192 frame = wireprotoframing.readframe(req.bodyfh)
193 if not frame:
193 if not frame:
194 break
194 break
195
195
196 action, meta = reactor.onframerecv(frame)
196 action, meta = reactor.onframerecv(frame)
197
197
198 if action == 'wantframe':
198 if action == 'wantframe':
199 # Need more data before we can do anything.
199 # Need more data before we can do anything.
200 continue
200 continue
201 elif action == 'runcommand':
201 elif action == 'runcommand':
202 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
202 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
203 reqcommand, reactor, outstream,
203 reqcommand, reactor, outstream,
204 meta, issubsequent=seencommand)
204 meta, issubsequent=seencommand)
205
205
206 if sentoutput:
206 if sentoutput:
207 return
207 return
208
208
209 seencommand = True
209 seencommand = True
210
210
211 elif action == 'error':
211 elif action == 'error':
212 # TODO define proper error mechanism.
212 # TODO define proper error mechanism.
213 res.status = b'200 OK'
213 res.status = b'200 OK'
214 res.headers[b'Content-Type'] = b'text/plain'
214 res.headers[b'Content-Type'] = b'text/plain'
215 res.setbodybytes(meta['message'] + b'\n')
215 res.setbodybytes(meta['message'] + b'\n')
216 return
216 return
217 else:
217 else:
218 raise error.ProgrammingError(
218 raise error.ProgrammingError(
219 'unhandled action from frame processor: %s' % action)
219 'unhandled action from frame processor: %s' % action)
220
220
221 action, meta = reactor.oninputeof()
221 action, meta = reactor.oninputeof()
222 if action == 'sendframes':
222 if action == 'sendframes':
223 # We assume we haven't started sending the response yet. If we're
223 # We assume we haven't started sending the response yet. If we're
224 # wrong, the response type will raise an exception.
224 # wrong, the response type will raise an exception.
225 res.status = b'200 OK'
225 res.status = b'200 OK'
226 res.headers[b'Content-Type'] = FRAMINGTYPE
226 res.headers[b'Content-Type'] = FRAMINGTYPE
227 res.setbodygen(meta['framegen'])
227 res.setbodygen(meta['framegen'])
228 elif action == 'noop':
228 elif action == 'noop':
229 pass
229 pass
230 else:
230 else:
231 raise error.ProgrammingError('unhandled action from frame processor: %s'
231 raise error.ProgrammingError('unhandled action from frame processor: %s'
232 % action)
232 % action)
233
233
234 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
234 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
235 outstream, command, issubsequent):
235 outstream, command, issubsequent):
236 """Dispatch a wire protocol command made from HTTPv2 requests.
236 """Dispatch a wire protocol command made from HTTPv2 requests.
237
237
238 The authenticated permission (``authedperm``) along with the original
238 The authenticated permission (``authedperm``) along with the original
239 command from the URL (``reqcommand``) are passed in.
239 command from the URL (``reqcommand``) are passed in.
240 """
240 """
241 # We already validated that the session has permissions to perform the
241 # We already validated that the session has permissions to perform the
242 # actions in ``authedperm``. In the unified frame protocol, the canonical
242 # actions in ``authedperm``. In the unified frame protocol, the canonical
243 # command to run is expressed in a frame. However, the URL also requested
243 # command to run is expressed in a frame. However, the URL also requested
244 # to run a specific command. We need to be careful that the command we
244 # to run a specific command. We need to be careful that the command we
245 # run doesn't have permissions requirements greater than what was granted
245 # run doesn't have permissions requirements greater than what was granted
246 # by ``authedperm``.
246 # by ``authedperm``.
247 #
247 #
248 # Our rule for this is we only allow one command per HTTP request and
248 # Our rule for this is we only allow one command per HTTP request and
249 # that command must match the command in the URL. However, we make
249 # that command must match the command in the URL. However, we make
250 # an exception for the ``multirequest`` URL. This URL is allowed to
250 # an exception for the ``multirequest`` URL. This URL is allowed to
251 # execute multiple commands. We double check permissions of each command
251 # execute multiple commands. We double check permissions of each command
252 # as it is invoked to ensure there is no privilege escalation.
252 # as it is invoked to ensure there is no privilege escalation.
253 # TODO consider allowing multiple commands to regular command URLs
253 # TODO consider allowing multiple commands to regular command URLs
254 # iff each command is the same.
254 # iff each command is the same.
255
255
256 proto = httpv2protocolhandler(req, ui, args=command['args'])
256 proto = httpv2protocolhandler(req, ui, args=command['args'])
257
257
258 if reqcommand == b'multirequest':
258 if reqcommand == b'multirequest':
259 if not COMMANDS.commandavailable(command['command'], proto):
259 if not COMMANDS.commandavailable(command['command'], proto):
260 # TODO proper error mechanism
260 # TODO proper error mechanism
261 res.status = b'200 OK'
261 res.status = b'200 OK'
262 res.headers[b'Content-Type'] = b'text/plain'
262 res.headers[b'Content-Type'] = b'text/plain'
263 res.setbodybytes(_('wire protocol command not available: %s') %
263 res.setbodybytes(_('wire protocol command not available: %s') %
264 command['command'])
264 command['command'])
265 return True
265 return True
266
266
267 # TODO don't use assert here, since it may be elided by -O.
267 # TODO don't use assert here, since it may be elided by -O.
268 assert authedperm in (b'ro', b'rw')
268 assert authedperm in (b'ro', b'rw')
269 wirecommand = COMMANDS[command['command']]
269 wirecommand = COMMANDS[command['command']]
270 assert wirecommand.permission in ('push', 'pull')
270 assert wirecommand.permission in ('push', 'pull')
271
271
272 if authedperm == b'ro' and wirecommand.permission != 'pull':
272 if authedperm == b'ro' and wirecommand.permission != 'pull':
273 # TODO proper error mechanism
273 # TODO proper error mechanism
274 res.status = b'403 Forbidden'
274 res.status = b'403 Forbidden'
275 res.headers[b'Content-Type'] = b'text/plain'
275 res.headers[b'Content-Type'] = b'text/plain'
276 res.setbodybytes(_('insufficient permissions to execute '
276 res.setbodybytes(_('insufficient permissions to execute '
277 'command: %s') % command['command'])
277 'command: %s') % command['command'])
278 return True
278 return True
279
279
280 # TODO should we also call checkperm() here? Maybe not if we're going
280 # TODO should we also call checkperm() here? Maybe not if we're going
281 # to overhaul that API. The granted scope from the URL check should
281 # to overhaul that API. The granted scope from the URL check should
282 # be good enough.
282 # be good enough.
283
283
284 else:
284 else:
285 # Don't allow multiple commands outside of ``multirequest`` URL.
285 # Don't allow multiple commands outside of ``multirequest`` URL.
286 if issubsequent:
286 if issubsequent:
287 # TODO proper error mechanism
287 # TODO proper error mechanism
288 res.status = b'200 OK'
288 res.status = b'200 OK'
289 res.headers[b'Content-Type'] = b'text/plain'
289 res.headers[b'Content-Type'] = b'text/plain'
290 res.setbodybytes(_('multiple commands cannot be issued to this '
290 res.setbodybytes(_('multiple commands cannot be issued to this '
291 'URL'))
291 'URL'))
292 return True
292 return True
293
293
294 if reqcommand != command['command']:
294 if reqcommand != command['command']:
295 # TODO define proper error mechanism
295 # TODO define proper error mechanism
296 res.status = b'200 OK'
296 res.status = b'200 OK'
297 res.headers[b'Content-Type'] = b'text/plain'
297 res.headers[b'Content-Type'] = b'text/plain'
298 res.setbodybytes(_('command in frame must match command in URL'))
298 res.setbodybytes(_('command in frame must match command in URL'))
299 return True
299 return True
300
300
301 res.status = b'200 OK'
301 res.status = b'200 OK'
302 res.headers[b'Content-Type'] = FRAMINGTYPE
302 res.headers[b'Content-Type'] = FRAMINGTYPE
303
303
304 try:
304 try:
305 objs = dispatch(repo, proto, command['command'])
305 objs = dispatch(repo, proto, command['command'])
306
306
307 action, meta = reactor.oncommandresponsereadyobjects(
307 action, meta = reactor.oncommandresponsereadyobjects(
308 outstream, command['requestid'], objs)
308 outstream, command['requestid'], objs)
309
309
310 except Exception as e:
310 except Exception as e:
311 action, meta = reactor.onservererror(
311 action, meta = reactor.onservererror(
312 outstream, command['requestid'],
312 outstream, command['requestid'],
313 _('exception when invoking command: %s') % e)
313 _('exception when invoking command: %s') % e)
314
314
315 if action == 'sendframes':
315 if action == 'sendframes':
316 res.setbodygen(meta['framegen'])
316 res.setbodygen(meta['framegen'])
317 return True
317 return True
318 elif action == 'noop':
318 elif action == 'noop':
319 return False
319 return False
320 else:
320 else:
321 raise error.ProgrammingError('unhandled event from reactor: %s' %
321 raise error.ProgrammingError('unhandled event from reactor: %s' %
322 action)
322 action)
323
323
324 def getdispatchrepo(repo, proto, command):
324 def getdispatchrepo(repo, proto, command):
325 return repo.filtered('served')
325 return repo.filtered('served')
326
326
327 def dispatch(repo, proto, command):
327 def dispatch(repo, proto, command):
328 repo = getdispatchrepo(repo, proto, command)
328 repo = getdispatchrepo(repo, proto, command)
329
329
330 func, spec = COMMANDS[command]
330 func, spec = COMMANDS[command]
331 args = proto.getargs(spec)
331 args = proto.getargs(spec)
332
332
333 return func(repo, proto, **args)
333 return func(repo, proto, **args)
334
334
335 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
335 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
336 class httpv2protocolhandler(object):
336 class httpv2protocolhandler(object):
337 def __init__(self, req, ui, args=None):
337 def __init__(self, req, ui, args=None):
338 self._req = req
338 self._req = req
339 self._ui = ui
339 self._ui = ui
340 self._args = args
340 self._args = args
341
341
342 @property
342 @property
343 def name(self):
343 def name(self):
344 return HTTP_WIREPROTO_V2
344 return HTTP_WIREPROTO_V2
345
345
346 def getargs(self, args):
346 def getargs(self, args):
347 data = {}
347 data = {}
348 for k, typ in args.items():
348 for k, typ in args.items():
349 if k == '*':
349 if k == '*':
350 raise NotImplementedError('do not support * args')
350 raise NotImplementedError('do not support * args')
351 elif k in self._args:
351 elif k in self._args:
352 # TODO consider validating value types.
352 # TODO consider validating value types.
353 data[k] = self._args[k]
353 data[k] = self._args[k]
354
354
355 return data
355 return data
356
356
357 def getprotocaps(self):
357 def getprotocaps(self):
358 # Protocol capabilities are currently not implemented for HTTP V2.
358 # Protocol capabilities are currently not implemented for HTTP V2.
359 return set()
359 return set()
360
360
361 def getpayload(self):
361 def getpayload(self):
362 raise NotImplementedError
362 raise NotImplementedError
363
363
364 @contextlib.contextmanager
364 @contextlib.contextmanager
365 def mayberedirectstdio(self):
365 def mayberedirectstdio(self):
366 raise NotImplementedError
366 raise NotImplementedError
367
367
368 def client(self):
368 def client(self):
369 raise NotImplementedError
369 raise NotImplementedError
370
370
371 def addcapabilities(self, repo, caps):
371 def addcapabilities(self, repo, caps):
372 return caps
372 return caps
373
373
374 def checkperm(self, perm):
374 def checkperm(self, perm):
375 raise NotImplementedError
375 raise NotImplementedError
376
376
377 def httpv2apidescriptor(req, repo):
377 def httpv2apidescriptor(req, repo):
378 proto = httpv2protocolhandler(req, repo.ui)
378 proto = httpv2protocolhandler(req, repo.ui)
379
379
380 return _capabilitiesv2(repo, proto)
380 return _capabilitiesv2(repo, proto)
381
381
382 def _capabilitiesv2(repo, proto):
382 def _capabilitiesv2(repo, proto):
383 """Obtain the set of capabilities for version 2 transports.
383 """Obtain the set of capabilities for version 2 transports.
384
384
385 These capabilities are distinct from the capabilities for version 1
385 These capabilities are distinct from the capabilities for version 1
386 transports.
386 transports.
387 """
387 """
388 compression = []
388 compression = []
389 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
389 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
390 compression.append({
390 compression.append({
391 b'name': engine.wireprotosupport().name,
391 b'name': engine.wireprotosupport().name,
392 })
392 })
393
393
394 caps = {
394 caps = {
395 'commands': {},
395 'commands': {},
396 'compression': compression,
396 'compression': compression,
397 'framingmediatypes': [FRAMINGTYPE],
397 'framingmediatypes': [FRAMINGTYPE],
398 }
398 }
399
399
400 # TODO expose available changesetdata fields.
401
400 for command, entry in COMMANDS.items():
402 for command, entry in COMMANDS.items():
401 caps['commands'][command] = {
403 caps['commands'][command] = {
402 'args': entry.args,
404 'args': entry.args,
403 'permissions': [entry.permission],
405 'permissions': [entry.permission],
404 }
406 }
405
407
406 if streamclone.allowservergeneration(repo):
408 if streamclone.allowservergeneration(repo):
407 caps['rawrepoformats'] = sorted(repo.requirements &
409 caps['rawrepoformats'] = sorted(repo.requirements &
408 repo.supportedformats)
410 repo.supportedformats)
409
411
410 return proto.addcapabilities(repo, caps)
412 return proto.addcapabilities(repo, caps)
411
413
412 def wireprotocommand(name, args=None, permission='push'):
414 def wireprotocommand(name, args=None, permission='push'):
413 """Decorator to declare a wire protocol command.
415 """Decorator to declare a wire protocol command.
414
416
415 ``name`` is the name of the wire protocol command being provided.
417 ``name`` is the name of the wire protocol command being provided.
416
418
417 ``args`` is a dict of argument names to example values.
419 ``args`` is a dict of argument names to example values.
418
420
419 ``permission`` defines the permission type needed to run this command.
421 ``permission`` defines the permission type needed to run this command.
420 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
422 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
421 respectively. Default is to assume command requires ``push`` permissions
423 respectively. Default is to assume command requires ``push`` permissions
422 because otherwise commands not declaring their permissions could modify
424 because otherwise commands not declaring their permissions could modify
423 a repository that is supposed to be read-only.
425 a repository that is supposed to be read-only.
424
426
425 Wire protocol commands are generators of objects to be serialized and
427 Wire protocol commands are generators of objects to be serialized and
426 sent to the client.
428 sent to the client.
427
429
428 If a command raises an uncaught exception, this will be translated into
430 If a command raises an uncaught exception, this will be translated into
429 a command error.
431 a command error.
430 """
432 """
431 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
433 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
432 if v['version'] == 2}
434 if v['version'] == 2}
433
435
434 if permission not in ('push', 'pull'):
436 if permission not in ('push', 'pull'):
435 raise error.ProgrammingError('invalid wire protocol permission; '
437 raise error.ProgrammingError('invalid wire protocol permission; '
436 'got %s; expected "push" or "pull"' %
438 'got %s; expected "push" or "pull"' %
437 permission)
439 permission)
438
440
439 if args is None:
441 if args is None:
440 args = {}
442 args = {}
441
443
442 if not isinstance(args, dict):
444 if not isinstance(args, dict):
443 raise error.ProgrammingError('arguments for version 2 commands '
445 raise error.ProgrammingError('arguments for version 2 commands '
444 'must be declared as dicts')
446 'must be declared as dicts')
445
447
446 def register(func):
448 def register(func):
447 if name in COMMANDS:
449 if name in COMMANDS:
448 raise error.ProgrammingError('%s command already registered '
450 raise error.ProgrammingError('%s command already registered '
449 'for version 2' % name)
451 'for version 2' % name)
450
452
451 COMMANDS[name] = wireprototypes.commandentry(
453 COMMANDS[name] = wireprototypes.commandentry(
452 func, args=args, transports=transports, permission=permission)
454 func, args=args, transports=transports, permission=permission)
453
455
454 return func
456 return func
455
457
456 return register
458 return register
457
459
458 @wireprotocommand('branchmap', permission='pull')
460 @wireprotocommand('branchmap', permission='pull')
459 def branchmapv2(repo, proto):
461 def branchmapv2(repo, proto):
460 yield {encoding.fromlocal(k): v
462 yield {encoding.fromlocal(k): v
461 for k, v in repo.branchmap().iteritems()}
463 for k, v in repo.branchmap().iteritems()}
462
464
463 @wireprotocommand('capabilities', permission='pull')
465 @wireprotocommand('capabilities', permission='pull')
464 def capabilitiesv2(repo, proto):
466 def capabilitiesv2(repo, proto):
465 yield _capabilitiesv2(repo, proto)
467 yield _capabilitiesv2(repo, proto)
466
468
467 @wireprotocommand('changesetdata',
469 @wireprotocommand('changesetdata',
468 args={
470 args={
469 'noderange': [[b'0123456...'], [b'abcdef...']],
471 'noderange': [[b'0123456...'], [b'abcdef...']],
470 'nodes': [b'0123456...'],
472 'nodes': [b'0123456...'],
471 'fields': {b'parents', b'revision'},
473 'fields': {b'parents', b'revision'},
472 },
474 },
473 permission='pull')
475 permission='pull')
474 def changesetdata(repo, proto, noderange=None, nodes=None, fields=None):
476 def changesetdata(repo, proto, noderange=None, nodes=None, fields=None):
475 fields = fields or set()
477 fields = fields or set()
476
478
479 # TODO look for unknown fields and abort when they can't be serviced.
480
477 if noderange is None and nodes is None:
481 if noderange is None and nodes is None:
478 raise error.WireprotoCommandError(
482 raise error.WireprotoCommandError(
479 'noderange or nodes must be defined')
483 'noderange or nodes must be defined')
480
484
481 if noderange is not None:
485 if noderange is not None:
482 if len(noderange) != 2:
486 if len(noderange) != 2:
483 raise error.WireprotoCommandError(
487 raise error.WireprotoCommandError(
484 'noderange must consist of 2 elements')
488 'noderange must consist of 2 elements')
485
489
486 if not noderange[1]:
490 if not noderange[1]:
487 raise error.WireprotoCommandError(
491 raise error.WireprotoCommandError(
488 'heads in noderange request cannot be empty')
492 'heads in noderange request cannot be empty')
489
493
490 cl = repo.changelog
494 cl = repo.changelog
491 hasnode = cl.hasnode
495 hasnode = cl.hasnode
492
496
493 seen = set()
497 seen = set()
494 outgoing = []
498 outgoing = []
495
499
496 if nodes is not None:
500 if nodes is not None:
497 outgoing.extend(n for n in nodes if hasnode(n))
501 outgoing.extend(n for n in nodes if hasnode(n))
498 seen |= set(outgoing)
502 seen |= set(outgoing)
499
503
500 if noderange is not None:
504 if noderange is not None:
501 if noderange[0]:
505 if noderange[0]:
502 common = [n for n in noderange[0] if hasnode(n)]
506 common = [n for n in noderange[0] if hasnode(n)]
503 else:
507 else:
504 common = [nullid]
508 common = [nullid]
505
509
506 for n in discovery.outgoing(repo, common, noderange[1]).missing:
510 for n in discovery.outgoing(repo, common, noderange[1]).missing:
507 if n not in seen:
511 if n not in seen:
508 outgoing.append(n)
512 outgoing.append(n)
509 # Don't need to add to seen here because this is the final
513 # Don't need to add to seen here because this is the final
510 # source of nodes and there should be no duplicates in this
514 # source of nodes and there should be no duplicates in this
511 # list.
515 # list.
512
516
513 seen.clear()
517 seen.clear()
514 publishing = repo.publishing()
518 publishing = repo.publishing()
515
519
516 if outgoing:
520 if outgoing:
517 repo.hook('preoutgoing', throw=True, source='serve')
521 repo.hook('preoutgoing', throw=True, source='serve')
518
522
519 yield {
523 yield {
520 b'totalitems': len(outgoing),
524 b'totalitems': len(outgoing),
521 }
525 }
522
526
523 # The phases of nodes already transferred to the client may have changed
527 # The phases of nodes already transferred to the client may have changed
524 # since the client last requested data. We send phase-only records
528 # since the client last requested data. We send phase-only records
525 # for these revisions, if requested.
529 # for these revisions, if requested.
526 if b'phase' in fields and noderange is not None:
530 if b'phase' in fields and noderange is not None:
527 # TODO skip nodes whose phase will be reflected by a node in the
531 # TODO skip nodes whose phase will be reflected by a node in the
528 # outgoing set. This is purely an optimization to reduce data
532 # outgoing set. This is purely an optimization to reduce data
529 # size.
533 # size.
530 for node in noderange[0]:
534 for node in noderange[0]:
531 yield {
535 yield {
532 b'node': node,
536 b'node': node,
533 b'phase': b'public' if publishing else repo[node].phasestr()
537 b'phase': b'public' if publishing else repo[node].phasestr()
534 }
538 }
535
539
536 nodebookmarks = {}
540 nodebookmarks = {}
537 for mark, node in repo._bookmarks.items():
541 for mark, node in repo._bookmarks.items():
538 nodebookmarks.setdefault(node, set()).add(mark)
542 nodebookmarks.setdefault(node, set()).add(mark)
539
543
540 # It is already topologically sorted by revision number.
544 # It is already topologically sorted by revision number.
541 for node in outgoing:
545 for node in outgoing:
542 d = {
546 d = {
543 b'node': node,
547 b'node': node,
544 }
548 }
545
549
546 if b'parents' in fields:
550 if b'parents' in fields:
547 d[b'parents'] = cl.parents(node)
551 d[b'parents'] = cl.parents(node)
548
552
549 if b'phase' in fields:
553 if b'phase' in fields:
550 if publishing:
554 if publishing:
551 d[b'phase'] = b'public'
555 d[b'phase'] = b'public'
552 else:
556 else:
553 ctx = repo[node]
557 ctx = repo[node]
554 d[b'phase'] = ctx.phasestr()
558 d[b'phase'] = ctx.phasestr()
555
559
556 if b'bookmarks' in fields and node in nodebookmarks:
560 if b'bookmarks' in fields and node in nodebookmarks:
557 d[b'bookmarks'] = sorted(nodebookmarks[node])
561 d[b'bookmarks'] = sorted(nodebookmarks[node])
558 del nodebookmarks[node]
562 del nodebookmarks[node]
559
563
560 revisiondata = None
564 revisiondata = None
561
565
562 if b'revision' in fields:
566 if b'revision' in fields:
563 revisiondata = cl.revision(node, raw=True)
567 revisiondata = cl.revision(node, raw=True)
564 d[b'revisionsize'] = len(revisiondata)
568 d[b'revisionsize'] = len(revisiondata)
565
569
570 # TODO make it possible for extensions to wrap a function or register
571 # a handler to service custom fields.
572
566 yield d
573 yield d
567
574
568 if revisiondata is not None:
575 if revisiondata is not None:
569 yield revisiondata
576 yield revisiondata
570
577
571 # If requested, send bookmarks from nodes that didn't have revision
578 # If requested, send bookmarks from nodes that didn't have revision
572 # data sent so receiver is aware of any bookmark updates.
579 # data sent so receiver is aware of any bookmark updates.
573 if b'bookmarks' in fields:
580 if b'bookmarks' in fields:
574 for node, marks in sorted(nodebookmarks.iteritems()):
581 for node, marks in sorted(nodebookmarks.iteritems()):
575 yield {
582 yield {
576 b'node': node,
583 b'node': node,
577 b'bookmarks': sorted(marks),
584 b'bookmarks': sorted(marks),
578 }
585 }
579
586
580 @wireprotocommand('heads',
587 @wireprotocommand('heads',
581 args={
588 args={
582 'publiconly': False,
589 'publiconly': False,
583 },
590 },
584 permission='pull')
591 permission='pull')
585 def headsv2(repo, proto, publiconly=False):
592 def headsv2(repo, proto, publiconly=False):
586 if publiconly:
593 if publiconly:
587 repo = repo.filtered('immutable')
594 repo = repo.filtered('immutable')
588
595
589 yield repo.heads()
596 yield repo.heads()
590
597
591 @wireprotocommand('known',
598 @wireprotocommand('known',
592 args={
599 args={
593 'nodes': [b'deadbeef'],
600 'nodes': [b'deadbeef'],
594 },
601 },
595 permission='pull')
602 permission='pull')
596 def knownv2(repo, proto, nodes=None):
603 def knownv2(repo, proto, nodes=None):
597 nodes = nodes or []
604 nodes = nodes or []
598 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
605 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
599 yield result
606 yield result
600
607
601 @wireprotocommand('listkeys',
608 @wireprotocommand('listkeys',
602 args={
609 args={
603 'namespace': b'ns',
610 'namespace': b'ns',
604 },
611 },
605 permission='pull')
612 permission='pull')
606 def listkeysv2(repo, proto, namespace=None):
613 def listkeysv2(repo, proto, namespace=None):
607 keys = repo.listkeys(encoding.tolocal(namespace))
614 keys = repo.listkeys(encoding.tolocal(namespace))
608 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
615 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
609 for k, v in keys.iteritems()}
616 for k, v in keys.iteritems()}
610
617
611 yield keys
618 yield keys
612
619
613 @wireprotocommand('lookup',
620 @wireprotocommand('lookup',
614 args={
621 args={
615 'key': b'foo',
622 'key': b'foo',
616 },
623 },
617 permission='pull')
624 permission='pull')
618 def lookupv2(repo, proto, key):
625 def lookupv2(repo, proto, key):
619 key = encoding.tolocal(key)
626 key = encoding.tolocal(key)
620
627
621 # TODO handle exception.
628 # TODO handle exception.
622 node = repo.lookup(key)
629 node = repo.lookup(key)
623
630
624 yield node
631 yield node
625
632
626 @wireprotocommand('pushkey',
633 @wireprotocommand('pushkey',
627 args={
634 args={
628 'namespace': b'ns',
635 'namespace': b'ns',
629 'key': b'key',
636 'key': b'key',
630 'old': b'old',
637 'old': b'old',
631 'new': b'new',
638 'new': b'new',
632 },
639 },
633 permission='push')
640 permission='push')
634 def pushkeyv2(repo, proto, namespace, key, old, new):
641 def pushkeyv2(repo, proto, namespace, key, old, new):
635 # TODO handle ui output redirection
642 # TODO handle ui output redirection
636 yield repo.pushkey(encoding.tolocal(namespace),
643 yield repo.pushkey(encoding.tolocal(namespace),
637 encoding.tolocal(key),
644 encoding.tolocal(key),
638 encoding.tolocal(old),
645 encoding.tolocal(old),
639 encoding.tolocal(new))
646 encoding.tolocal(new))
General Comments 0
You need to be logged in to leave comments. Login now