##// END OF EJS Templates
narrow: move the ellipses server capability to core...
Pulkit Goyal -
r39970:a24f4638 default
parent child Browse files
Show More
@@ -1,421 +1,417 b''
1 # narrowcommands.py - command modifications for narrowhg extension
1 # narrowcommands.py - command modifications for narrowhg extension
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import itertools
9 import itertools
10 import os
10 import os
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 cmdutil,
14 cmdutil,
15 commands,
15 commands,
16 discovery,
16 discovery,
17 encoding,
17 encoding,
18 error,
18 error,
19 exchange,
19 exchange,
20 extensions,
20 extensions,
21 hg,
21 hg,
22 merge,
22 merge,
23 narrowspec,
23 narrowspec,
24 node,
24 node,
25 pycompat,
25 pycompat,
26 registrar,
26 registrar,
27 repair,
27 repair,
28 repository,
28 repository,
29 repoview,
29 repoview,
30 sparse,
30 sparse,
31 util,
31 util,
32 wireprotoserver,
32 wireprotoserver,
33 )
33 )
34
34
35 from . import (
36 narrowwirepeer,
37 )
38
39 table = {}
35 table = {}
40 command = registrar.command(table)
36 command = registrar.command(table)
41
37
42 def setup():
38 def setup():
43 """Wraps user-facing mercurial commands with narrow-aware versions."""
39 """Wraps user-facing mercurial commands with narrow-aware versions."""
44
40
45 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
41 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
46 entry[1].append(('', 'narrow', None,
42 entry[1].append(('', 'narrow', None,
47 _("create a narrow clone of select files")))
43 _("create a narrow clone of select files")))
48 entry[1].append(('', 'depth', '',
44 entry[1].append(('', 'depth', '',
49 _("limit the history fetched by distance from heads")))
45 _("limit the history fetched by distance from heads")))
50 entry[1].append(('', 'narrowspec', '',
46 entry[1].append(('', 'narrowspec', '',
51 _("read narrowspecs from file")))
47 _("read narrowspecs from file")))
52 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
48 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
53 if 'sparse' not in extensions.enabled():
49 if 'sparse' not in extensions.enabled():
54 entry[1].append(('', 'include', [],
50 entry[1].append(('', 'include', [],
55 _("specifically fetch this file/directory")))
51 _("specifically fetch this file/directory")))
56 entry[1].append(
52 entry[1].append(
57 ('', 'exclude', [],
53 ('', 'exclude', [],
58 _("do not fetch this file/directory, even if included")))
54 _("do not fetch this file/directory, even if included")))
59
55
60 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
56 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
61 entry[1].append(('', 'depth', '',
57 entry[1].append(('', 'depth', '',
62 _("limit the history fetched by distance from heads")))
58 _("limit the history fetched by distance from heads")))
63
59
64 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
60 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
65
61
66 def clonenarrowcmd(orig, ui, repo, *args, **opts):
62 def clonenarrowcmd(orig, ui, repo, *args, **opts):
67 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
63 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
68 opts = pycompat.byteskwargs(opts)
64 opts = pycompat.byteskwargs(opts)
69 wrappedextraprepare = util.nullcontextmanager()
65 wrappedextraprepare = util.nullcontextmanager()
70 narrowspecfile = opts['narrowspec']
66 narrowspecfile = opts['narrowspec']
71
67
72 if narrowspecfile:
68 if narrowspecfile:
73 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
69 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
74 ui.status(_("reading narrowspec from '%s'\n") % filepath)
70 ui.status(_("reading narrowspec from '%s'\n") % filepath)
75 try:
71 try:
76 fdata = util.readfile(filepath)
72 fdata = util.readfile(filepath)
77 except IOError as inst:
73 except IOError as inst:
78 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
74 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
79 (filepath, encoding.strtolocal(inst.strerror)))
75 (filepath, encoding.strtolocal(inst.strerror)))
80
76
81 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
77 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
82 if profiles:
78 if profiles:
83 raise error.Abort(_("cannot specify other files using '%include' in"
79 raise error.Abort(_("cannot specify other files using '%include' in"
84 " narrowspec"))
80 " narrowspec"))
85
81
86 narrowspec.validatepatterns(includes)
82 narrowspec.validatepatterns(includes)
87 narrowspec.validatepatterns(excludes)
83 narrowspec.validatepatterns(excludes)
88
84
89 # narrowspec is passed so we should assume that user wants narrow clone
85 # narrowspec is passed so we should assume that user wants narrow clone
90 opts['narrow'] = True
86 opts['narrow'] = True
91 opts['include'].extend(includes)
87 opts['include'].extend(includes)
92 opts['exclude'].extend(excludes)
88 opts['exclude'].extend(excludes)
93
89
94 if opts['narrow']:
90 if opts['narrow']:
95 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
91 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
96 orig(pullop, kwargs)
92 orig(pullop, kwargs)
97
93
98 if opts.get('depth'):
94 if opts.get('depth'):
99 kwargs['depth'] = opts['depth']
95 kwargs['depth'] = opts['depth']
100 wrappedextraprepare = extensions.wrappedfunction(exchange,
96 wrappedextraprepare = extensions.wrappedfunction(exchange,
101 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
97 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
102
98
103 with wrappedextraprepare:
99 with wrappedextraprepare:
104 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
100 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
105
101
106 def pullnarrowcmd(orig, ui, repo, *args, **opts):
102 def pullnarrowcmd(orig, ui, repo, *args, **opts):
107 """Wraps pull command to allow modifying narrow spec."""
103 """Wraps pull command to allow modifying narrow spec."""
108 wrappedextraprepare = util.nullcontextmanager()
104 wrappedextraprepare = util.nullcontextmanager()
109 if repository.NARROW_REQUIREMENT in repo.requirements:
105 if repository.NARROW_REQUIREMENT in repo.requirements:
110
106
111 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
107 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
112 orig(pullop, kwargs)
108 orig(pullop, kwargs)
113 if opts.get(r'depth'):
109 if opts.get(r'depth'):
114 kwargs['depth'] = opts[r'depth']
110 kwargs['depth'] = opts[r'depth']
115 wrappedextraprepare = extensions.wrappedfunction(exchange,
111 wrappedextraprepare = extensions.wrappedfunction(exchange,
116 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
112 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
117
113
118 with wrappedextraprepare:
114 with wrappedextraprepare:
119 return orig(ui, repo, *args, **opts)
115 return orig(ui, repo, *args, **opts)
120
116
121 def archivenarrowcmd(orig, ui, repo, *args, **opts):
117 def archivenarrowcmd(orig, ui, repo, *args, **opts):
122 """Wraps archive command to narrow the default includes."""
118 """Wraps archive command to narrow the default includes."""
123 if repository.NARROW_REQUIREMENT in repo.requirements:
119 if repository.NARROW_REQUIREMENT in repo.requirements:
124 repo_includes, repo_excludes = repo.narrowpats
120 repo_includes, repo_excludes = repo.narrowpats
125 includes = set(opts.get(r'include', []))
121 includes = set(opts.get(r'include', []))
126 excludes = set(opts.get(r'exclude', []))
122 excludes = set(opts.get(r'exclude', []))
127 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
123 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
128 includes, excludes, repo_includes, repo_excludes)
124 includes, excludes, repo_includes, repo_excludes)
129 if includes:
125 if includes:
130 opts[r'include'] = includes
126 opts[r'include'] = includes
131 if excludes:
127 if excludes:
132 opts[r'exclude'] = excludes
128 opts[r'exclude'] = excludes
133 return orig(ui, repo, *args, **opts)
129 return orig(ui, repo, *args, **opts)
134
130
135 def pullbundle2extraprepare(orig, pullop, kwargs):
131 def pullbundle2extraprepare(orig, pullop, kwargs):
136 repo = pullop.repo
132 repo = pullop.repo
137 if repository.NARROW_REQUIREMENT not in repo.requirements:
133 if repository.NARROW_REQUIREMENT not in repo.requirements:
138 return orig(pullop, kwargs)
134 return orig(pullop, kwargs)
139
135
140 if wireprotoserver.NARROWCAP not in pullop.remote.capabilities():
136 if wireprotoserver.NARROWCAP not in pullop.remote.capabilities():
141 raise error.Abort(_("server doesn't support narrow clones"))
137 raise error.Abort(_("server doesn't support narrow clones"))
142 orig(pullop, kwargs)
138 orig(pullop, kwargs)
143 kwargs['narrow'] = True
139 kwargs['narrow'] = True
144 include, exclude = repo.narrowpats
140 include, exclude = repo.narrowpats
145 kwargs['oldincludepats'] = include
141 kwargs['oldincludepats'] = include
146 kwargs['oldexcludepats'] = exclude
142 kwargs['oldexcludepats'] = exclude
147 kwargs['includepats'] = include
143 kwargs['includepats'] = include
148 kwargs['excludepats'] = exclude
144 kwargs['excludepats'] = exclude
149 # calculate known nodes only in ellipses cases because in non-ellipses cases
145 # calculate known nodes only in ellipses cases because in non-ellipses cases
150 # we have all the nodes
146 # we have all the nodes
151 if narrowwirepeer.ELLIPSESCAP in pullop.remote.capabilities():
147 if wireprotoserver.ELLIPSESCAP in pullop.remote.capabilities():
152 kwargs['known'] = [node.hex(ctx.node()) for ctx in
148 kwargs['known'] = [node.hex(ctx.node()) for ctx in
153 repo.set('::%ln', pullop.common)
149 repo.set('::%ln', pullop.common)
154 if ctx.node() != node.nullid]
150 if ctx.node() != node.nullid]
155 if not kwargs['known']:
151 if not kwargs['known']:
156 # Mercurial serializes an empty list as '' and deserializes it as
152 # Mercurial serializes an empty list as '' and deserializes it as
157 # [''], so delete it instead to avoid handling the empty string on
153 # [''], so delete it instead to avoid handling the empty string on
158 # the server.
154 # the server.
159 del kwargs['known']
155 del kwargs['known']
160
156
161 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
157 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
162 pullbundle2extraprepare)
158 pullbundle2extraprepare)
163
159
164 # This is an extension point for filesystems that need to do something other
160 # This is an extension point for filesystems that need to do something other
165 # than just blindly unlink the files. It's not clear what arguments would be
161 # than just blindly unlink the files. It's not clear what arguments would be
166 # useful, so we're passing in a fair number of them, some of them redundant.
162 # useful, so we're passing in a fair number of them, some of them redundant.
167 def _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes, newexcludes,
163 def _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes, newexcludes,
168 oldmatch, newmatch):
164 oldmatch, newmatch):
169 for f in repo.dirstate:
165 for f in repo.dirstate:
170 if not newmatch(f):
166 if not newmatch(f):
171 repo.dirstate.drop(f)
167 repo.dirstate.drop(f)
172 repo.wvfs.unlinkpath(f)
168 repo.wvfs.unlinkpath(f)
173
169
174 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
170 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
175 newincludes, newexcludes, force):
171 newincludes, newexcludes, force):
176 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
172 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
177 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
173 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
178
174
179 # This is essentially doing "hg outgoing" to find all local-only
175 # This is essentially doing "hg outgoing" to find all local-only
180 # commits. We will then check that the local-only commits don't
176 # commits. We will then check that the local-only commits don't
181 # have any changes to files that will be untracked.
177 # have any changes to files that will be untracked.
182 unfi = repo.unfiltered()
178 unfi = repo.unfiltered()
183 outgoing = discovery.findcommonoutgoing(unfi, remote,
179 outgoing = discovery.findcommonoutgoing(unfi, remote,
184 commoninc=commoninc)
180 commoninc=commoninc)
185 ui.status(_('looking for local changes to affected paths\n'))
181 ui.status(_('looking for local changes to affected paths\n'))
186 localnodes = []
182 localnodes = []
187 for n in itertools.chain(outgoing.missing, outgoing.excluded):
183 for n in itertools.chain(outgoing.missing, outgoing.excluded):
188 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
184 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
189 localnodes.append(n)
185 localnodes.append(n)
190 revstostrip = unfi.revs('descendants(%ln)', localnodes)
186 revstostrip = unfi.revs('descendants(%ln)', localnodes)
191 hiddenrevs = repoview.filterrevs(repo, 'visible')
187 hiddenrevs = repoview.filterrevs(repo, 'visible')
192 visibletostrip = list(repo.changelog.node(r)
188 visibletostrip = list(repo.changelog.node(r)
193 for r in (revstostrip - hiddenrevs))
189 for r in (revstostrip - hiddenrevs))
194 if visibletostrip:
190 if visibletostrip:
195 ui.status(_('The following changeset(s) or their ancestors have '
191 ui.status(_('The following changeset(s) or their ancestors have '
196 'local changes not on the remote:\n'))
192 'local changes not on the remote:\n'))
197 maxnodes = 10
193 maxnodes = 10
198 if ui.verbose or len(visibletostrip) <= maxnodes:
194 if ui.verbose or len(visibletostrip) <= maxnodes:
199 for n in visibletostrip:
195 for n in visibletostrip:
200 ui.status('%s\n' % node.short(n))
196 ui.status('%s\n' % node.short(n))
201 else:
197 else:
202 for n in visibletostrip[:maxnodes]:
198 for n in visibletostrip[:maxnodes]:
203 ui.status('%s\n' % node.short(n))
199 ui.status('%s\n' % node.short(n))
204 ui.status(_('...and %d more, use --verbose to list all\n') %
200 ui.status(_('...and %d more, use --verbose to list all\n') %
205 (len(visibletostrip) - maxnodes))
201 (len(visibletostrip) - maxnodes))
206 if not force:
202 if not force:
207 raise error.Abort(_('local changes found'),
203 raise error.Abort(_('local changes found'),
208 hint=_('use --force-delete-local-changes to '
204 hint=_('use --force-delete-local-changes to '
209 'ignore'))
205 'ignore'))
210
206
211 with ui.uninterruptable():
207 with ui.uninterruptable():
212 if revstostrip:
208 if revstostrip:
213 tostrip = [unfi.changelog.node(r) for r in revstostrip]
209 tostrip = [unfi.changelog.node(r) for r in revstostrip]
214 if repo['.'].node() in tostrip:
210 if repo['.'].node() in tostrip:
215 # stripping working copy, so move to a different commit first
211 # stripping working copy, so move to a different commit first
216 urev = max(repo.revs('(::%n) - %ln + null',
212 urev = max(repo.revs('(::%n) - %ln + null',
217 repo['.'].node(), visibletostrip))
213 repo['.'].node(), visibletostrip))
218 hg.clean(repo, urev)
214 hg.clean(repo, urev)
219 repair.strip(ui, unfi, tostrip, topic='narrow')
215 repair.strip(ui, unfi, tostrip, topic='narrow')
220
216
221 todelete = []
217 todelete = []
222 for f, f2, size in repo.store.datafiles():
218 for f, f2, size in repo.store.datafiles():
223 if f.startswith('data/'):
219 if f.startswith('data/'):
224 file = f[5:-2]
220 file = f[5:-2]
225 if not newmatch(file):
221 if not newmatch(file):
226 todelete.append(f)
222 todelete.append(f)
227 elif f.startswith('meta/'):
223 elif f.startswith('meta/'):
228 dir = f[5:-13]
224 dir = f[5:-13]
229 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
225 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
230 include = True
226 include = True
231 for d in dirs:
227 for d in dirs:
232 visit = newmatch.visitdir(d)
228 visit = newmatch.visitdir(d)
233 if not visit:
229 if not visit:
234 include = False
230 include = False
235 break
231 break
236 if visit == 'all':
232 if visit == 'all':
237 break
233 break
238 if not include:
234 if not include:
239 todelete.append(f)
235 todelete.append(f)
240
236
241 repo.destroying()
237 repo.destroying()
242
238
243 with repo.transaction("narrowing"):
239 with repo.transaction("narrowing"):
244 for f in todelete:
240 for f in todelete:
245 ui.status(_('deleting %s\n') % f)
241 ui.status(_('deleting %s\n') % f)
246 util.unlinkpath(repo.svfs.join(f))
242 util.unlinkpath(repo.svfs.join(f))
247 repo.store.markremoved(f)
243 repo.store.markremoved(f)
248
244
249 _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes,
245 _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes,
250 newexcludes, oldmatch, newmatch)
246 newexcludes, oldmatch, newmatch)
251 repo.setnarrowpats(newincludes, newexcludes)
247 repo.setnarrowpats(newincludes, newexcludes)
252
248
253 repo.destroyed()
249 repo.destroyed()
254
250
255 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
251 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
256 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
252 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
257
253
258 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
254 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
259 orig(pullop, kwargs)
255 orig(pullop, kwargs)
260 # The old{in,ex}cludepats have already been set by orig()
256 # The old{in,ex}cludepats have already been set by orig()
261 kwargs['includepats'] = newincludes
257 kwargs['includepats'] = newincludes
262 kwargs['excludepats'] = newexcludes
258 kwargs['excludepats'] = newexcludes
263 kwargs['widen'] = True
259 kwargs['widen'] = True
264 wrappedextraprepare = extensions.wrappedfunction(exchange,
260 wrappedextraprepare = extensions.wrappedfunction(exchange,
265 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
261 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
266
262
267 # define a function that narrowbundle2 can call after creating the
263 # define a function that narrowbundle2 can call after creating the
268 # backup bundle, but before applying the bundle from the server
264 # backup bundle, but before applying the bundle from the server
269 def setnewnarrowpats():
265 def setnewnarrowpats():
270 repo.setnarrowpats(newincludes, newexcludes)
266 repo.setnarrowpats(newincludes, newexcludes)
271 repo.setnewnarrowpats = setnewnarrowpats
267 repo.setnewnarrowpats = setnewnarrowpats
272 # silence the devel-warning of applying an empty changegroup
268 # silence the devel-warning of applying an empty changegroup
273 overrides = {('devel', 'all-warnings'): False}
269 overrides = {('devel', 'all-warnings'): False}
274
270
275 with ui.uninterruptable():
271 with ui.uninterruptable():
276 ds = repo.dirstate
272 ds = repo.dirstate
277 p1, p2 = ds.p1(), ds.p2()
273 p1, p2 = ds.p1(), ds.p2()
278 with ds.parentchange():
274 with ds.parentchange():
279 ds.setparents(node.nullid, node.nullid)
275 ds.setparents(node.nullid, node.nullid)
280 common = commoninc[0]
276 common = commoninc[0]
281 with wrappedextraprepare, repo.ui.configoverride(overrides, 'widen'):
277 with wrappedextraprepare, repo.ui.configoverride(overrides, 'widen'):
282 exchange.pull(repo, remote, heads=common)
278 exchange.pull(repo, remote, heads=common)
283 with ds.parentchange():
279 with ds.parentchange():
284 ds.setparents(p1, p2)
280 ds.setparents(p1, p2)
285
281
286 repo.setnewnarrowpats()
282 repo.setnewnarrowpats()
287 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
283 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
288 addgaction = actions['g'].append
284 addgaction = actions['g'].append
289
285
290 mf = repo['.'].manifest().matches(newmatch)
286 mf = repo['.'].manifest().matches(newmatch)
291 for f, fn in mf.iteritems():
287 for f, fn in mf.iteritems():
292 if f not in repo.dirstate:
288 if f not in repo.dirstate:
293 addgaction((f, (mf.flags(f), False),
289 addgaction((f, (mf.flags(f), False),
294 "add from widened narrow clone"))
290 "add from widened narrow clone"))
295
291
296 merge.applyupdates(repo, actions, wctx=repo[None],
292 merge.applyupdates(repo, actions, wctx=repo[None],
297 mctx=repo['.'], overwrite=False)
293 mctx=repo['.'], overwrite=False)
298 merge.recordupdates(repo, actions, branchmerge=False)
294 merge.recordupdates(repo, actions, branchmerge=False)
299
295
300 # TODO(rdamazio): Make new matcher format and update description
296 # TODO(rdamazio): Make new matcher format and update description
301 @command('tracked',
297 @command('tracked',
302 [('', 'addinclude', [], _('new paths to include')),
298 [('', 'addinclude', [], _('new paths to include')),
303 ('', 'removeinclude', [], _('old paths to no longer include')),
299 ('', 'removeinclude', [], _('old paths to no longer include')),
304 ('', 'addexclude', [], _('new paths to exclude')),
300 ('', 'addexclude', [], _('new paths to exclude')),
305 ('', 'import-rules', '', _('import narrowspecs from a file')),
301 ('', 'import-rules', '', _('import narrowspecs from a file')),
306 ('', 'removeexclude', [], _('old paths to no longer exclude')),
302 ('', 'removeexclude', [], _('old paths to no longer exclude')),
307 ('', 'clear', False, _('whether to replace the existing narrowspec')),
303 ('', 'clear', False, _('whether to replace the existing narrowspec')),
308 ('', 'force-delete-local-changes', False,
304 ('', 'force-delete-local-changes', False,
309 _('forces deletion of local changes when narrowing')),
305 _('forces deletion of local changes when narrowing')),
310 ] + commands.remoteopts,
306 ] + commands.remoteopts,
311 _('[OPTIONS]... [REMOTE]'),
307 _('[OPTIONS]... [REMOTE]'),
312 inferrepo=True)
308 inferrepo=True)
313 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
309 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
314 """show or change the current narrowspec
310 """show or change the current narrowspec
315
311
316 With no argument, shows the current narrowspec entries, one per line. Each
312 With no argument, shows the current narrowspec entries, one per line. Each
317 line will be prefixed with 'I' or 'X' for included or excluded patterns,
313 line will be prefixed with 'I' or 'X' for included or excluded patterns,
318 respectively.
314 respectively.
319
315
320 The narrowspec is comprised of expressions to match remote files and/or
316 The narrowspec is comprised of expressions to match remote files and/or
321 directories that should be pulled into your client.
317 directories that should be pulled into your client.
322 The narrowspec has *include* and *exclude* expressions, with excludes always
318 The narrowspec has *include* and *exclude* expressions, with excludes always
323 trumping includes: that is, if a file matches an exclude expression, it will
319 trumping includes: that is, if a file matches an exclude expression, it will
324 be excluded even if it also matches an include expression.
320 be excluded even if it also matches an include expression.
325 Excluding files that were never included has no effect.
321 Excluding files that were never included has no effect.
326
322
327 Each included or excluded entry is in the format described by
323 Each included or excluded entry is in the format described by
328 'hg help patterns'.
324 'hg help patterns'.
329
325
330 The options allow you to add or remove included and excluded expressions.
326 The options allow you to add or remove included and excluded expressions.
331
327
332 If --clear is specified, then all previous includes and excludes are DROPPED
328 If --clear is specified, then all previous includes and excludes are DROPPED
333 and replaced by the new ones specified to --addinclude and --addexclude.
329 and replaced by the new ones specified to --addinclude and --addexclude.
334 If --clear is specified without any further options, the narrowspec will be
330 If --clear is specified without any further options, the narrowspec will be
335 empty and will not match any files.
331 empty and will not match any files.
336 """
332 """
337 opts = pycompat.byteskwargs(opts)
333 opts = pycompat.byteskwargs(opts)
338 if repository.NARROW_REQUIREMENT not in repo.requirements:
334 if repository.NARROW_REQUIREMENT not in repo.requirements:
339 ui.warn(_('The narrow command is only supported on respositories cloned'
335 ui.warn(_('The narrow command is only supported on respositories cloned'
340 ' with --narrow.\n'))
336 ' with --narrow.\n'))
341 return 1
337 return 1
342
338
343 # Before supporting, decide whether it "hg tracked --clear" should mean
339 # Before supporting, decide whether it "hg tracked --clear" should mean
344 # tracking no paths or all paths.
340 # tracking no paths or all paths.
345 if opts['clear']:
341 if opts['clear']:
346 ui.warn(_('The --clear option is not yet supported.\n'))
342 ui.warn(_('The --clear option is not yet supported.\n'))
347 return 1
343 return 1
348
344
349 # import rules from a file
345 # import rules from a file
350 newrules = opts.get('import_rules')
346 newrules = opts.get('import_rules')
351 if newrules:
347 if newrules:
352 try:
348 try:
353 filepath = os.path.join(encoding.getcwd(), newrules)
349 filepath = os.path.join(encoding.getcwd(), newrules)
354 fdata = util.readfile(filepath)
350 fdata = util.readfile(filepath)
355 except IOError as inst:
351 except IOError as inst:
356 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
352 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
357 (filepath, encoding.strtolocal(inst.strerror)))
353 (filepath, encoding.strtolocal(inst.strerror)))
358 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
354 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
359 'narrow')
355 'narrow')
360 if profiles:
356 if profiles:
361 raise error.Abort(_("including other spec files using '%include' "
357 raise error.Abort(_("including other spec files using '%include' "
362 "is not supported in narrowspec"))
358 "is not supported in narrowspec"))
363 opts['addinclude'].extend(includepats)
359 opts['addinclude'].extend(includepats)
364 opts['addexclude'].extend(excludepats)
360 opts['addexclude'].extend(excludepats)
365
361
366 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
362 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
367 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
363 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
368 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
364 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
369 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
365 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
370 widening = addedincludes or removedexcludes
366 widening = addedincludes or removedexcludes
371 narrowing = removedincludes or addedexcludes
367 narrowing = removedincludes or addedexcludes
372 only_show = not widening and not narrowing
368 only_show = not widening and not narrowing
373
369
374 # Only print the current narrowspec.
370 # Only print the current narrowspec.
375 if only_show:
371 if only_show:
376 include, exclude = repo.narrowpats
372 include, exclude = repo.narrowpats
377
373
378 ui.pager('tracked')
374 ui.pager('tracked')
379 fm = ui.formatter('narrow', opts)
375 fm = ui.formatter('narrow', opts)
380 for i in sorted(include):
376 for i in sorted(include):
381 fm.startitem()
377 fm.startitem()
382 fm.write('status', '%s ', 'I', label='narrow.included')
378 fm.write('status', '%s ', 'I', label='narrow.included')
383 fm.write('pat', '%s\n', i, label='narrow.included')
379 fm.write('pat', '%s\n', i, label='narrow.included')
384 for i in sorted(exclude):
380 for i in sorted(exclude):
385 fm.startitem()
381 fm.startitem()
386 fm.write('status', '%s ', 'X', label='narrow.excluded')
382 fm.write('status', '%s ', 'X', label='narrow.excluded')
387 fm.write('pat', '%s\n', i, label='narrow.excluded')
383 fm.write('pat', '%s\n', i, label='narrow.excluded')
388 fm.end()
384 fm.end()
389 return 0
385 return 0
390
386
391 with repo.wlock(), repo.lock():
387 with repo.wlock(), repo.lock():
392 cmdutil.bailifchanged(repo)
388 cmdutil.bailifchanged(repo)
393
389
394 # Find the revisions we have in common with the remote. These will
390 # Find the revisions we have in common with the remote. These will
395 # be used for finding local-only changes for narrowing. They will
391 # be used for finding local-only changes for narrowing. They will
396 # also define the set of revisions to update for widening.
392 # also define the set of revisions to update for widening.
397 remotepath = ui.expandpath(remotepath or 'default')
393 remotepath = ui.expandpath(remotepath or 'default')
398 url, branches = hg.parseurl(remotepath)
394 url, branches = hg.parseurl(remotepath)
399 ui.status(_('comparing with %s\n') % util.hidepassword(url))
395 ui.status(_('comparing with %s\n') % util.hidepassword(url))
400 remote = hg.peer(repo, opts, url)
396 remote = hg.peer(repo, opts, url)
401 commoninc = discovery.findcommonincoming(repo, remote)
397 commoninc = discovery.findcommonincoming(repo, remote)
402
398
403 oldincludes, oldexcludes = repo.narrowpats
399 oldincludes, oldexcludes = repo.narrowpats
404 if narrowing:
400 if narrowing:
405 newincludes = oldincludes - removedincludes
401 newincludes = oldincludes - removedincludes
406 newexcludes = oldexcludes | addedexcludes
402 newexcludes = oldexcludes | addedexcludes
407 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
403 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
408 newincludes, newexcludes,
404 newincludes, newexcludes,
409 opts['force_delete_local_changes'])
405 opts['force_delete_local_changes'])
410 # _narrow() updated the narrowspec and _widen() below needs to
406 # _narrow() updated the narrowspec and _widen() below needs to
411 # use the updated values as its base (otherwise removed includes
407 # use the updated values as its base (otherwise removed includes
412 # and addedexcludes will be lost in the resulting narrowspec)
408 # and addedexcludes will be lost in the resulting narrowspec)
413 oldincludes = newincludes
409 oldincludes = newincludes
414 oldexcludes = newexcludes
410 oldexcludes = newexcludes
415
411
416 if widening:
412 if widening:
417 newincludes = oldincludes | addedincludes
413 newincludes = oldincludes | addedincludes
418 newexcludes = oldexcludes - removedexcludes
414 newexcludes = oldexcludes - removedexcludes
419 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
415 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
420
416
421 return 0
417 return 0
@@ -1,34 +1,33 b''
1 # narrowrepo.py - repository which supports narrow revlogs, lazy loading
1 # narrowrepo.py - repository which supports narrow revlogs, lazy loading
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
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 from mercurial import (
10 from mercurial import (
11 wireprotoserver,
11 wireprotoserver,
12 )
12 )
13
13
14 from . import (
14 from . import (
15 narrowdirstate,
15 narrowdirstate,
16 narrowwirepeer,
17 )
16 )
18
17
19 def wraprepo(repo):
18 def wraprepo(repo):
20 """Enables narrow clone functionality on a single local repository."""
19 """Enables narrow clone functionality on a single local repository."""
21
20
22 class narrowrepository(repo.__class__):
21 class narrowrepository(repo.__class__):
23
22
24 def _makedirstate(self):
23 def _makedirstate(self):
25 dirstate = super(narrowrepository, self)._makedirstate()
24 dirstate = super(narrowrepository, self)._makedirstate()
26 return narrowdirstate.wrapdirstate(self, dirstate)
25 return narrowdirstate.wrapdirstate(self, dirstate)
27
26
28 def peer(self):
27 def peer(self):
29 peer = super(narrowrepository, self).peer()
28 peer = super(narrowrepository, self).peer()
30 peer._caps.add(wireprotoserver.NARROWCAP)
29 peer._caps.add(wireprotoserver.NARROWCAP)
31 peer._caps.add(narrowwirepeer.ELLIPSESCAP)
30 peer._caps.add(wireprotoserver.ELLIPSESCAP)
32 return peer
31 return peer
33
32
34 repo.__class__ = narrowrepository
33 repo.__class__ = narrowrepository
@@ -1,41 +1,39 b''
1 # narrowwirepeer.py - passes narrow spec with unbundle command
1 # narrowwirepeer.py - passes narrow spec with unbundle command
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
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 from mercurial import (
10 from mercurial import (
11 extensions,
11 extensions,
12 hg,
12 hg,
13 wireprotoserver,
13 wireprotoserver,
14 wireprotov1server,
14 wireprotov1server,
15 )
15 )
16
16
17 ELLIPSESCAP = 'exp-ellipses-1'
18
19 def uisetup():
17 def uisetup():
20 extensions.wrapfunction(wireprotov1server, '_capabilities', addnarrowcap)
18 extensions.wrapfunction(wireprotov1server, '_capabilities', addnarrowcap)
21
19
22 def addnarrowcap(orig, repo, proto):
20 def addnarrowcap(orig, repo, proto):
23 """add the narrow capability to the server"""
21 """add the narrow capability to the server"""
24 caps = orig(repo, proto)
22 caps = orig(repo, proto)
25 caps.append(wireprotoserver.NARROWCAP)
23 caps.append(wireprotoserver.NARROWCAP)
26 if repo.ui.configbool('experimental', 'narrowservebrokenellipses'):
24 if repo.ui.configbool('experimental', 'narrowservebrokenellipses'):
27 caps.append(ELLIPSESCAP)
25 caps.append(wireprotoserver.ELLIPSESCAP)
28 return caps
26 return caps
29
27
30 def reposetup(repo):
28 def reposetup(repo):
31 def wirereposetup(ui, peer):
29 def wirereposetup(ui, peer):
32 def wrapped(orig, cmd, *args, **kwargs):
30 def wrapped(orig, cmd, *args, **kwargs):
33 if cmd == 'unbundle':
31 if cmd == 'unbundle':
34 # TODO: don't blindly add include/exclude wireproto
32 # TODO: don't blindly add include/exclude wireproto
35 # arguments to unbundle.
33 # arguments to unbundle.
36 include, exclude = repo.narrowpats
34 include, exclude = repo.narrowpats
37 kwargs[r"includepats"] = ','.join(include)
35 kwargs[r"includepats"] = ','.join(include)
38 kwargs[r"excludepats"] = ','.join(exclude)
36 kwargs[r"excludepats"] = ','.join(exclude)
39 return orig(cmd, *args, **kwargs)
37 return orig(cmd, *args, **kwargs)
40 extensions.wrapfunction(peer, '_calltwowaystream', wrapped)
38 extensions.wrapfunction(peer, '_calltwowaystream', wrapped)
41 hg.wirepeersetupfuncs.append(wirereposetup)
39 hg.wirepeersetupfuncs.append(wirereposetup)
@@ -1,806 +1,807 b''
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 import struct
10 import struct
11 import sys
11 import sys
12 import threading
12 import threading
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 wireprototypes,
20 wireprototypes,
21 wireprotov1server,
21 wireprotov1server,
22 wireprotov2server,
22 wireprotov2server,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 cborutil,
25 cborutil,
26 interfaceutil,
26 interfaceutil,
27 procutil,
27 procutil,
28 )
28 )
29
29
30 stringio = util.stringio
30 stringio = util.stringio
31
31
32 urlerr = util.urlerr
32 urlerr = util.urlerr
33 urlreq = util.urlreq
33 urlreq = util.urlreq
34
34
35 HTTP_OK = 200
35 HTTP_OK = 200
36
36
37 HGTYPE = 'application/mercurial-0.1'
37 HGTYPE = 'application/mercurial-0.1'
38 HGTYPE2 = 'application/mercurial-0.2'
38 HGTYPE2 = 'application/mercurial-0.2'
39 HGERRTYPE = 'application/hg-error'
39 HGERRTYPE = 'application/hg-error'
40
40
41 NARROWCAP = 'exp-narrow-1'
41 NARROWCAP = 'exp-narrow-1'
42 ELLIPSESCAP = 'exp-ellipses-1'
42
43
43 SSHV1 = wireprototypes.SSHV1
44 SSHV1 = wireprototypes.SSHV1
44 SSHV2 = wireprototypes.SSHV2
45 SSHV2 = wireprototypes.SSHV2
45
46
46 def decodevaluefromheaders(req, headerprefix):
47 def decodevaluefromheaders(req, headerprefix):
47 """Decode a long value from multiple HTTP request headers.
48 """Decode a long value from multiple HTTP request headers.
48
49
49 Returns the value as a bytes, not a str.
50 Returns the value as a bytes, not a str.
50 """
51 """
51 chunks = []
52 chunks = []
52 i = 1
53 i = 1
53 while True:
54 while True:
54 v = req.headers.get(b'%s-%d' % (headerprefix, i))
55 v = req.headers.get(b'%s-%d' % (headerprefix, i))
55 if v is None:
56 if v is None:
56 break
57 break
57 chunks.append(pycompat.bytesurl(v))
58 chunks.append(pycompat.bytesurl(v))
58 i += 1
59 i += 1
59
60
60 return ''.join(chunks)
61 return ''.join(chunks)
61
62
62 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
63 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
63 class httpv1protocolhandler(object):
64 class httpv1protocolhandler(object):
64 def __init__(self, req, ui, checkperm):
65 def __init__(self, req, ui, checkperm):
65 self._req = req
66 self._req = req
66 self._ui = ui
67 self._ui = ui
67 self._checkperm = checkperm
68 self._checkperm = checkperm
68 self._protocaps = None
69 self._protocaps = None
69
70
70 @property
71 @property
71 def name(self):
72 def name(self):
72 return 'http-v1'
73 return 'http-v1'
73
74
74 def getargs(self, args):
75 def getargs(self, args):
75 knownargs = self._args()
76 knownargs = self._args()
76 data = {}
77 data = {}
77 keys = args.split()
78 keys = args.split()
78 for k in keys:
79 for k in keys:
79 if k == '*':
80 if k == '*':
80 star = {}
81 star = {}
81 for key in knownargs.keys():
82 for key in knownargs.keys():
82 if key != 'cmd' and key not in keys:
83 if key != 'cmd' and key not in keys:
83 star[key] = knownargs[key][0]
84 star[key] = knownargs[key][0]
84 data['*'] = star
85 data['*'] = star
85 else:
86 else:
86 data[k] = knownargs[k][0]
87 data[k] = knownargs[k][0]
87 return [data[k] for k in keys]
88 return [data[k] for k in keys]
88
89
89 def _args(self):
90 def _args(self):
90 args = self._req.qsparams.asdictoflists()
91 args = self._req.qsparams.asdictoflists()
91 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
92 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
92 if postlen:
93 if postlen:
93 args.update(urlreq.parseqs(
94 args.update(urlreq.parseqs(
94 self._req.bodyfh.read(postlen), keep_blank_values=True))
95 self._req.bodyfh.read(postlen), keep_blank_values=True))
95 return args
96 return args
96
97
97 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
98 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
98 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
99 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
99 return args
100 return args
100
101
101 def getprotocaps(self):
102 def getprotocaps(self):
102 if self._protocaps is None:
103 if self._protocaps is None:
103 value = decodevaluefromheaders(self._req, b'X-HgProto')
104 value = decodevaluefromheaders(self._req, b'X-HgProto')
104 self._protocaps = set(value.split(' '))
105 self._protocaps = set(value.split(' '))
105 return self._protocaps
106 return self._protocaps
106
107
107 def getpayload(self):
108 def getpayload(self):
108 # Existing clients *always* send Content-Length.
109 # Existing clients *always* send Content-Length.
109 length = int(self._req.headers[b'Content-Length'])
110 length = int(self._req.headers[b'Content-Length'])
110
111
111 # If httppostargs is used, we need to read Content-Length
112 # If httppostargs is used, we need to read Content-Length
112 # minus the amount that was consumed by args.
113 # minus the amount that was consumed by args.
113 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
114 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
114 return util.filechunkiter(self._req.bodyfh, limit=length)
115 return util.filechunkiter(self._req.bodyfh, limit=length)
115
116
116 @contextlib.contextmanager
117 @contextlib.contextmanager
117 def mayberedirectstdio(self):
118 def mayberedirectstdio(self):
118 oldout = self._ui.fout
119 oldout = self._ui.fout
119 olderr = self._ui.ferr
120 olderr = self._ui.ferr
120
121
121 out = util.stringio()
122 out = util.stringio()
122
123
123 try:
124 try:
124 self._ui.fout = out
125 self._ui.fout = out
125 self._ui.ferr = out
126 self._ui.ferr = out
126 yield out
127 yield out
127 finally:
128 finally:
128 self._ui.fout = oldout
129 self._ui.fout = oldout
129 self._ui.ferr = olderr
130 self._ui.ferr = olderr
130
131
131 def client(self):
132 def client(self):
132 return 'remote:%s:%s:%s' % (
133 return 'remote:%s:%s:%s' % (
133 self._req.urlscheme,
134 self._req.urlscheme,
134 urlreq.quote(self._req.remotehost or ''),
135 urlreq.quote(self._req.remotehost or ''),
135 urlreq.quote(self._req.remoteuser or ''))
136 urlreq.quote(self._req.remoteuser or ''))
136
137
137 def addcapabilities(self, repo, caps):
138 def addcapabilities(self, repo, caps):
138 caps.append(b'batch')
139 caps.append(b'batch')
139
140
140 caps.append('httpheader=%d' %
141 caps.append('httpheader=%d' %
141 repo.ui.configint('server', 'maxhttpheaderlen'))
142 repo.ui.configint('server', 'maxhttpheaderlen'))
142 if repo.ui.configbool('experimental', 'httppostargs'):
143 if repo.ui.configbool('experimental', 'httppostargs'):
143 caps.append('httppostargs')
144 caps.append('httppostargs')
144
145
145 # FUTURE advertise 0.2rx once support is implemented
146 # FUTURE advertise 0.2rx once support is implemented
146 # FUTURE advertise minrx and mintx after consulting config option
147 # FUTURE advertise minrx and mintx after consulting config option
147 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
148 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
148
149
149 compengines = wireprototypes.supportedcompengines(repo.ui,
150 compengines = wireprototypes.supportedcompengines(repo.ui,
150 util.SERVERROLE)
151 util.SERVERROLE)
151 if compengines:
152 if compengines:
152 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
153 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
153 for e in compengines)
154 for e in compengines)
154 caps.append('compression=%s' % comptypes)
155 caps.append('compression=%s' % comptypes)
155
156
156 return caps
157 return caps
157
158
158 def checkperm(self, perm):
159 def checkperm(self, perm):
159 return self._checkperm(perm)
160 return self._checkperm(perm)
160
161
161 # This method exists mostly so that extensions like remotefilelog can
162 # This method exists mostly so that extensions like remotefilelog can
162 # disable a kludgey legacy method only over http. As of early 2018,
163 # disable a kludgey legacy method only over http. As of early 2018,
163 # there are no other known users, so with any luck we can discard this
164 # there are no other known users, so with any luck we can discard this
164 # hook if remotefilelog becomes a first-party extension.
165 # hook if remotefilelog becomes a first-party extension.
165 def iscmd(cmd):
166 def iscmd(cmd):
166 return cmd in wireprotov1server.commands
167 return cmd in wireprotov1server.commands
167
168
168 def handlewsgirequest(rctx, req, res, checkperm):
169 def handlewsgirequest(rctx, req, res, checkperm):
169 """Possibly process a wire protocol request.
170 """Possibly process a wire protocol request.
170
171
171 If the current request is a wire protocol request, the request is
172 If the current request is a wire protocol request, the request is
172 processed by this function.
173 processed by this function.
173
174
174 ``req`` is a ``parsedrequest`` instance.
175 ``req`` is a ``parsedrequest`` instance.
175 ``res`` is a ``wsgiresponse`` instance.
176 ``res`` is a ``wsgiresponse`` instance.
176
177
177 Returns a bool indicating if the request was serviced. If set, the caller
178 Returns a bool indicating if the request was serviced. If set, the caller
178 should stop processing the request, as a response has already been issued.
179 should stop processing the request, as a response has already been issued.
179 """
180 """
180 # Avoid cycle involving hg module.
181 # Avoid cycle involving hg module.
181 from .hgweb import common as hgwebcommon
182 from .hgweb import common as hgwebcommon
182
183
183 repo = rctx.repo
184 repo = rctx.repo
184
185
185 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
186 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
186 # string parameter. If it isn't present, this isn't a wire protocol
187 # string parameter. If it isn't present, this isn't a wire protocol
187 # request.
188 # request.
188 if 'cmd' not in req.qsparams:
189 if 'cmd' not in req.qsparams:
189 return False
190 return False
190
191
191 cmd = req.qsparams['cmd']
192 cmd = req.qsparams['cmd']
192
193
193 # The "cmd" request parameter is used by both the wire protocol and hgweb.
194 # The "cmd" request parameter is used by both the wire protocol and hgweb.
194 # While not all wire protocol commands are available for all transports,
195 # While not all wire protocol commands are available for all transports,
195 # if we see a "cmd" value that resembles a known wire protocol command, we
196 # if we see a "cmd" value that resembles a known wire protocol command, we
196 # route it to a protocol handler. This is better than routing possible
197 # route it to a protocol handler. This is better than routing possible
197 # wire protocol requests to hgweb because it prevents hgweb from using
198 # wire protocol requests to hgweb because it prevents hgweb from using
198 # known wire protocol commands and it is less confusing for machine
199 # known wire protocol commands and it is less confusing for machine
199 # clients.
200 # clients.
200 if not iscmd(cmd):
201 if not iscmd(cmd):
201 return False
202 return False
202
203
203 # The "cmd" query string argument is only valid on the root path of the
204 # The "cmd" query string argument is only valid on the root path of the
204 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
205 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
205 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
206 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
206 # in this case. We send an HTTP 404 for backwards compatibility reasons.
207 # in this case. We send an HTTP 404 for backwards compatibility reasons.
207 if req.dispatchpath:
208 if req.dispatchpath:
208 res.status = hgwebcommon.statusmessage(404)
209 res.status = hgwebcommon.statusmessage(404)
209 res.headers['Content-Type'] = HGTYPE
210 res.headers['Content-Type'] = HGTYPE
210 # TODO This is not a good response to issue for this request. This
211 # TODO This is not a good response to issue for this request. This
211 # is mostly for BC for now.
212 # is mostly for BC for now.
212 res.setbodybytes('0\n%s\n' % b'Not Found')
213 res.setbodybytes('0\n%s\n' % b'Not Found')
213 return True
214 return True
214
215
215 proto = httpv1protocolhandler(req, repo.ui,
216 proto = httpv1protocolhandler(req, repo.ui,
216 lambda perm: checkperm(rctx, req, perm))
217 lambda perm: checkperm(rctx, req, perm))
217
218
218 # The permissions checker should be the only thing that can raise an
219 # The permissions checker should be the only thing that can raise an
219 # ErrorResponse. It is kind of a layer violation to catch an hgweb
220 # ErrorResponse. It is kind of a layer violation to catch an hgweb
220 # exception here. So consider refactoring into a exception type that
221 # exception here. So consider refactoring into a exception type that
221 # is associated with the wire protocol.
222 # is associated with the wire protocol.
222 try:
223 try:
223 _callhttp(repo, req, res, proto, cmd)
224 _callhttp(repo, req, res, proto, cmd)
224 except hgwebcommon.ErrorResponse as e:
225 except hgwebcommon.ErrorResponse as e:
225 for k, v in e.headers:
226 for k, v in e.headers:
226 res.headers[k] = v
227 res.headers[k] = v
227 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
228 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
228 # TODO This response body assumes the failed command was
229 # TODO This response body assumes the failed command was
229 # "unbundle." That assumption is not always valid.
230 # "unbundle." That assumption is not always valid.
230 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
231 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
231
232
232 return True
233 return True
233
234
234 def _availableapis(repo):
235 def _availableapis(repo):
235 apis = set()
236 apis = set()
236
237
237 # Registered APIs are made available via config options of the name of
238 # Registered APIs are made available via config options of the name of
238 # the protocol.
239 # the protocol.
239 for k, v in API_HANDLERS.items():
240 for k, v in API_HANDLERS.items():
240 section, option = v['config']
241 section, option = v['config']
241 if repo.ui.configbool(section, option):
242 if repo.ui.configbool(section, option):
242 apis.add(k)
243 apis.add(k)
243
244
244 return apis
245 return apis
245
246
246 def handlewsgiapirequest(rctx, req, res, checkperm):
247 def handlewsgiapirequest(rctx, req, res, checkperm):
247 """Handle requests to /api/*."""
248 """Handle requests to /api/*."""
248 assert req.dispatchparts[0] == b'api'
249 assert req.dispatchparts[0] == b'api'
249
250
250 repo = rctx.repo
251 repo = rctx.repo
251
252
252 # This whole URL space is experimental for now. But we want to
253 # This whole URL space is experimental for now. But we want to
253 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
254 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
254 if not repo.ui.configbool('experimental', 'web.apiserver'):
255 if not repo.ui.configbool('experimental', 'web.apiserver'):
255 res.status = b'404 Not Found'
256 res.status = b'404 Not Found'
256 res.headers[b'Content-Type'] = b'text/plain'
257 res.headers[b'Content-Type'] = b'text/plain'
257 res.setbodybytes(_('Experimental API server endpoint not enabled'))
258 res.setbodybytes(_('Experimental API server endpoint not enabled'))
258 return
259 return
259
260
260 # The URL space is /api/<protocol>/*. The structure of URLs under varies
261 # The URL space is /api/<protocol>/*. The structure of URLs under varies
261 # by <protocol>.
262 # by <protocol>.
262
263
263 availableapis = _availableapis(repo)
264 availableapis = _availableapis(repo)
264
265
265 # Requests to /api/ list available APIs.
266 # Requests to /api/ list available APIs.
266 if req.dispatchparts == [b'api']:
267 if req.dispatchparts == [b'api']:
267 res.status = b'200 OK'
268 res.status = b'200 OK'
268 res.headers[b'Content-Type'] = b'text/plain'
269 res.headers[b'Content-Type'] = b'text/plain'
269 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
270 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
270 'one of the following:\n')]
271 'one of the following:\n')]
271 if availableapis:
272 if availableapis:
272 lines.extend(sorted(availableapis))
273 lines.extend(sorted(availableapis))
273 else:
274 else:
274 lines.append(_('(no available APIs)\n'))
275 lines.append(_('(no available APIs)\n'))
275 res.setbodybytes(b'\n'.join(lines))
276 res.setbodybytes(b'\n'.join(lines))
276 return
277 return
277
278
278 proto = req.dispatchparts[1]
279 proto = req.dispatchparts[1]
279
280
280 if proto not in API_HANDLERS:
281 if proto not in API_HANDLERS:
281 res.status = b'404 Not Found'
282 res.status = b'404 Not Found'
282 res.headers[b'Content-Type'] = b'text/plain'
283 res.headers[b'Content-Type'] = b'text/plain'
283 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
284 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
284 proto, b', '.join(sorted(availableapis))))
285 proto, b', '.join(sorted(availableapis))))
285 return
286 return
286
287
287 if proto not in availableapis:
288 if proto not in availableapis:
288 res.status = b'404 Not Found'
289 res.status = b'404 Not Found'
289 res.headers[b'Content-Type'] = b'text/plain'
290 res.headers[b'Content-Type'] = b'text/plain'
290 res.setbodybytes(_('API %s not enabled\n') % proto)
291 res.setbodybytes(_('API %s not enabled\n') % proto)
291 return
292 return
292
293
293 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
294 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
294 req.dispatchparts[2:])
295 req.dispatchparts[2:])
295
296
296 # Maps API name to metadata so custom API can be registered.
297 # Maps API name to metadata so custom API can be registered.
297 # Keys are:
298 # Keys are:
298 #
299 #
299 # config
300 # config
300 # Config option that controls whether service is enabled.
301 # Config option that controls whether service is enabled.
301 # handler
302 # handler
302 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
303 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
303 # when a request to this API is received.
304 # when a request to this API is received.
304 # apidescriptor
305 # apidescriptor
305 # Callable receiving (req, repo) that is called to obtain an API
306 # Callable receiving (req, repo) that is called to obtain an API
306 # descriptor for this service. The response must be serializable to CBOR.
307 # descriptor for this service. The response must be serializable to CBOR.
307 API_HANDLERS = {
308 API_HANDLERS = {
308 wireprotov2server.HTTP_WIREPROTO_V2: {
309 wireprotov2server.HTTP_WIREPROTO_V2: {
309 'config': ('experimental', 'web.api.http-v2'),
310 'config': ('experimental', 'web.api.http-v2'),
310 'handler': wireprotov2server.handlehttpv2request,
311 'handler': wireprotov2server.handlehttpv2request,
311 'apidescriptor': wireprotov2server.httpv2apidescriptor,
312 'apidescriptor': wireprotov2server.httpv2apidescriptor,
312 },
313 },
313 }
314 }
314
315
315 def _httpresponsetype(ui, proto, prefer_uncompressed):
316 def _httpresponsetype(ui, proto, prefer_uncompressed):
316 """Determine the appropriate response type and compression settings.
317 """Determine the appropriate response type and compression settings.
317
318
318 Returns a tuple of (mediatype, compengine, engineopts).
319 Returns a tuple of (mediatype, compengine, engineopts).
319 """
320 """
320 # Determine the response media type and compression engine based
321 # Determine the response media type and compression engine based
321 # on the request parameters.
322 # on the request parameters.
322
323
323 if '0.2' in proto.getprotocaps():
324 if '0.2' in proto.getprotocaps():
324 # All clients are expected to support uncompressed data.
325 # All clients are expected to support uncompressed data.
325 if prefer_uncompressed:
326 if prefer_uncompressed:
326 return HGTYPE2, util._noopengine(), {}
327 return HGTYPE2, util._noopengine(), {}
327
328
328 # Now find an agreed upon compression format.
329 # Now find an agreed upon compression format.
329 compformats = wireprotov1server.clientcompressionsupport(proto)
330 compformats = wireprotov1server.clientcompressionsupport(proto)
330 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
331 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
331 if engine.wireprotosupport().name in compformats:
332 if engine.wireprotosupport().name in compformats:
332 opts = {}
333 opts = {}
333 level = ui.configint('server', '%slevel' % engine.name())
334 level = ui.configint('server', '%slevel' % engine.name())
334 if level is not None:
335 if level is not None:
335 opts['level'] = level
336 opts['level'] = level
336
337
337 return HGTYPE2, engine, opts
338 return HGTYPE2, engine, opts
338
339
339 # No mutually supported compression format. Fall back to the
340 # No mutually supported compression format. Fall back to the
340 # legacy protocol.
341 # legacy protocol.
341
342
342 # Don't allow untrusted settings because disabling compression or
343 # Don't allow untrusted settings because disabling compression or
343 # setting a very high compression level could lead to flooding
344 # setting a very high compression level could lead to flooding
344 # the server's network or CPU.
345 # the server's network or CPU.
345 opts = {'level': ui.configint('server', 'zliblevel')}
346 opts = {'level': ui.configint('server', 'zliblevel')}
346 return HGTYPE, util.compengines['zlib'], opts
347 return HGTYPE, util.compengines['zlib'], opts
347
348
348 def processcapabilitieshandshake(repo, req, res, proto):
349 def processcapabilitieshandshake(repo, req, res, proto):
349 """Called during a ?cmd=capabilities request.
350 """Called during a ?cmd=capabilities request.
350
351
351 If the client is advertising support for a newer protocol, we send
352 If the client is advertising support for a newer protocol, we send
352 a CBOR response with information about available services. If no
353 a CBOR response with information about available services. If no
353 advertised services are available, we don't handle the request.
354 advertised services are available, we don't handle the request.
354 """
355 """
355 # Fall back to old behavior unless the API server is enabled.
356 # Fall back to old behavior unless the API server is enabled.
356 if not repo.ui.configbool('experimental', 'web.apiserver'):
357 if not repo.ui.configbool('experimental', 'web.apiserver'):
357 return False
358 return False
358
359
359 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
360 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
360 protocaps = decodevaluefromheaders(req, b'X-HgProto')
361 protocaps = decodevaluefromheaders(req, b'X-HgProto')
361 if not clientapis or not protocaps:
362 if not clientapis or not protocaps:
362 return False
363 return False
363
364
364 # We currently only support CBOR responses.
365 # We currently only support CBOR responses.
365 protocaps = set(protocaps.split(' '))
366 protocaps = set(protocaps.split(' '))
366 if b'cbor' not in protocaps:
367 if b'cbor' not in protocaps:
367 return False
368 return False
368
369
369 descriptors = {}
370 descriptors = {}
370
371
371 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
372 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
372 handler = API_HANDLERS[api]
373 handler = API_HANDLERS[api]
373
374
374 descriptorfn = handler.get('apidescriptor')
375 descriptorfn = handler.get('apidescriptor')
375 if not descriptorfn:
376 if not descriptorfn:
376 continue
377 continue
377
378
378 descriptors[api] = descriptorfn(req, repo)
379 descriptors[api] = descriptorfn(req, repo)
379
380
380 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
381 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
381 assert isinstance(v1caps, wireprototypes.bytesresponse)
382 assert isinstance(v1caps, wireprototypes.bytesresponse)
382
383
383 m = {
384 m = {
384 # TODO allow this to be configurable.
385 # TODO allow this to be configurable.
385 'apibase': 'api/',
386 'apibase': 'api/',
386 'apis': descriptors,
387 'apis': descriptors,
387 'v1capabilities': v1caps.data,
388 'v1capabilities': v1caps.data,
388 }
389 }
389
390
390 res.status = b'200 OK'
391 res.status = b'200 OK'
391 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
392 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
392 res.setbodybytes(b''.join(cborutil.streamencode(m)))
393 res.setbodybytes(b''.join(cborutil.streamencode(m)))
393
394
394 return True
395 return True
395
396
396 def _callhttp(repo, req, res, proto, cmd):
397 def _callhttp(repo, req, res, proto, cmd):
397 # Avoid cycle involving hg module.
398 # Avoid cycle involving hg module.
398 from .hgweb import common as hgwebcommon
399 from .hgweb import common as hgwebcommon
399
400
400 def genversion2(gen, engine, engineopts):
401 def genversion2(gen, engine, engineopts):
401 # application/mercurial-0.2 always sends a payload header
402 # application/mercurial-0.2 always sends a payload header
402 # identifying the compression engine.
403 # identifying the compression engine.
403 name = engine.wireprotosupport().name
404 name = engine.wireprotosupport().name
404 assert 0 < len(name) < 256
405 assert 0 < len(name) < 256
405 yield struct.pack('B', len(name))
406 yield struct.pack('B', len(name))
406 yield name
407 yield name
407
408
408 for chunk in gen:
409 for chunk in gen:
409 yield chunk
410 yield chunk
410
411
411 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
412 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
412 if code == HTTP_OK:
413 if code == HTTP_OK:
413 res.status = '200 Script output follows'
414 res.status = '200 Script output follows'
414 else:
415 else:
415 res.status = hgwebcommon.statusmessage(code)
416 res.status = hgwebcommon.statusmessage(code)
416
417
417 res.headers['Content-Type'] = contenttype
418 res.headers['Content-Type'] = contenttype
418
419
419 if bodybytes is not None:
420 if bodybytes is not None:
420 res.setbodybytes(bodybytes)
421 res.setbodybytes(bodybytes)
421 if bodygen is not None:
422 if bodygen is not None:
422 res.setbodygen(bodygen)
423 res.setbodygen(bodygen)
423
424
424 if not wireprotov1server.commands.commandavailable(cmd, proto):
425 if not wireprotov1server.commands.commandavailable(cmd, proto):
425 setresponse(HTTP_OK, HGERRTYPE,
426 setresponse(HTTP_OK, HGERRTYPE,
426 _('requested wire protocol command is not available over '
427 _('requested wire protocol command is not available over '
427 'HTTP'))
428 'HTTP'))
428 return
429 return
429
430
430 proto.checkperm(wireprotov1server.commands[cmd].permission)
431 proto.checkperm(wireprotov1server.commands[cmd].permission)
431
432
432 # Possibly handle a modern client wanting to switch protocols.
433 # Possibly handle a modern client wanting to switch protocols.
433 if (cmd == 'capabilities' and
434 if (cmd == 'capabilities' and
434 processcapabilitieshandshake(repo, req, res, proto)):
435 processcapabilitieshandshake(repo, req, res, proto)):
435
436
436 return
437 return
437
438
438 rsp = wireprotov1server.dispatch(repo, proto, cmd)
439 rsp = wireprotov1server.dispatch(repo, proto, cmd)
439
440
440 if isinstance(rsp, bytes):
441 if isinstance(rsp, bytes):
441 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
442 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
442 elif isinstance(rsp, wireprototypes.bytesresponse):
443 elif isinstance(rsp, wireprototypes.bytesresponse):
443 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
444 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
444 elif isinstance(rsp, wireprototypes.streamreslegacy):
445 elif isinstance(rsp, wireprototypes.streamreslegacy):
445 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
446 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
446 elif isinstance(rsp, wireprototypes.streamres):
447 elif isinstance(rsp, wireprototypes.streamres):
447 gen = rsp.gen
448 gen = rsp.gen
448
449
449 # This code for compression should not be streamres specific. It
450 # This code for compression should not be streamres specific. It
450 # is here because we only compress streamres at the moment.
451 # is here because we only compress streamres at the moment.
451 mediatype, engine, engineopts = _httpresponsetype(
452 mediatype, engine, engineopts = _httpresponsetype(
452 repo.ui, proto, rsp.prefer_uncompressed)
453 repo.ui, proto, rsp.prefer_uncompressed)
453 gen = engine.compressstream(gen, engineopts)
454 gen = engine.compressstream(gen, engineopts)
454
455
455 if mediatype == HGTYPE2:
456 if mediatype == HGTYPE2:
456 gen = genversion2(gen, engine, engineopts)
457 gen = genversion2(gen, engine, engineopts)
457
458
458 setresponse(HTTP_OK, mediatype, bodygen=gen)
459 setresponse(HTTP_OK, mediatype, bodygen=gen)
459 elif isinstance(rsp, wireprototypes.pushres):
460 elif isinstance(rsp, wireprototypes.pushres):
460 rsp = '%d\n%s' % (rsp.res, rsp.output)
461 rsp = '%d\n%s' % (rsp.res, rsp.output)
461 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
462 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
462 elif isinstance(rsp, wireprototypes.pusherr):
463 elif isinstance(rsp, wireprototypes.pusherr):
463 rsp = '0\n%s\n' % rsp.res
464 rsp = '0\n%s\n' % rsp.res
464 res.drain = True
465 res.drain = True
465 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
466 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
466 elif isinstance(rsp, wireprototypes.ooberror):
467 elif isinstance(rsp, wireprototypes.ooberror):
467 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
468 else:
469 else:
469 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
470
471
471 def _sshv1respondbytes(fout, value):
472 def _sshv1respondbytes(fout, value):
472 """Send a bytes response for protocol version 1."""
473 """Send a bytes response for protocol version 1."""
473 fout.write('%d\n' % len(value))
474 fout.write('%d\n' % len(value))
474 fout.write(value)
475 fout.write(value)
475 fout.flush()
476 fout.flush()
476
477
477 def _sshv1respondstream(fout, source):
478 def _sshv1respondstream(fout, source):
478 write = fout.write
479 write = fout.write
479 for chunk in source.gen:
480 for chunk in source.gen:
480 write(chunk)
481 write(chunk)
481 fout.flush()
482 fout.flush()
482
483
483 def _sshv1respondooberror(fout, ferr, rsp):
484 def _sshv1respondooberror(fout, ferr, rsp):
484 ferr.write(b'%s\n-\n' % rsp)
485 ferr.write(b'%s\n-\n' % rsp)
485 ferr.flush()
486 ferr.flush()
486 fout.write(b'\n')
487 fout.write(b'\n')
487 fout.flush()
488 fout.flush()
488
489
489 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
490 class sshv1protocolhandler(object):
491 class sshv1protocolhandler(object):
491 """Handler for requests services via version 1 of SSH protocol."""
492 """Handler for requests services via version 1 of SSH protocol."""
492 def __init__(self, ui, fin, fout):
493 def __init__(self, ui, fin, fout):
493 self._ui = ui
494 self._ui = ui
494 self._fin = fin
495 self._fin = fin
495 self._fout = fout
496 self._fout = fout
496 self._protocaps = set()
497 self._protocaps = set()
497
498
498 @property
499 @property
499 def name(self):
500 def name(self):
500 return wireprototypes.SSHV1
501 return wireprototypes.SSHV1
501
502
502 def getargs(self, args):
503 def getargs(self, args):
503 data = {}
504 data = {}
504 keys = args.split()
505 keys = args.split()
505 for n in pycompat.xrange(len(keys)):
506 for n in pycompat.xrange(len(keys)):
506 argline = self._fin.readline()[:-1]
507 argline = self._fin.readline()[:-1]
507 arg, l = argline.split()
508 arg, l = argline.split()
508 if arg not in keys:
509 if arg not in keys:
509 raise error.Abort(_("unexpected parameter %r") % arg)
510 raise error.Abort(_("unexpected parameter %r") % arg)
510 if arg == '*':
511 if arg == '*':
511 star = {}
512 star = {}
512 for k in pycompat.xrange(int(l)):
513 for k in pycompat.xrange(int(l)):
513 argline = self._fin.readline()[:-1]
514 argline = self._fin.readline()[:-1]
514 arg, l = argline.split()
515 arg, l = argline.split()
515 val = self._fin.read(int(l))
516 val = self._fin.read(int(l))
516 star[arg] = val
517 star[arg] = val
517 data['*'] = star
518 data['*'] = star
518 else:
519 else:
519 val = self._fin.read(int(l))
520 val = self._fin.read(int(l))
520 data[arg] = val
521 data[arg] = val
521 return [data[k] for k in keys]
522 return [data[k] for k in keys]
522
523
523 def getprotocaps(self):
524 def getprotocaps(self):
524 return self._protocaps
525 return self._protocaps
525
526
526 def getpayload(self):
527 def getpayload(self):
527 # We initially send an empty response. This tells the client it is
528 # We initially send an empty response. This tells the client it is
528 # OK to start sending data. If a client sees any other response, it
529 # OK to start sending data. If a client sees any other response, it
529 # interprets it as an error.
530 # interprets it as an error.
530 _sshv1respondbytes(self._fout, b'')
531 _sshv1respondbytes(self._fout, b'')
531
532
532 # The file is in the form:
533 # The file is in the form:
533 #
534 #
534 # <chunk size>\n<chunk>
535 # <chunk size>\n<chunk>
535 # ...
536 # ...
536 # 0\n
537 # 0\n
537 count = int(self._fin.readline())
538 count = int(self._fin.readline())
538 while count:
539 while count:
539 yield self._fin.read(count)
540 yield self._fin.read(count)
540 count = int(self._fin.readline())
541 count = int(self._fin.readline())
541
542
542 @contextlib.contextmanager
543 @contextlib.contextmanager
543 def mayberedirectstdio(self):
544 def mayberedirectstdio(self):
544 yield None
545 yield None
545
546
546 def client(self):
547 def client(self):
547 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
548 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
548 return 'remote:ssh:' + client
549 return 'remote:ssh:' + client
549
550
550 def addcapabilities(self, repo, caps):
551 def addcapabilities(self, repo, caps):
551 if self.name == wireprototypes.SSHV1:
552 if self.name == wireprototypes.SSHV1:
552 caps.append(b'protocaps')
553 caps.append(b'protocaps')
553 caps.append(b'batch')
554 caps.append(b'batch')
554 return caps
555 return caps
555
556
556 def checkperm(self, perm):
557 def checkperm(self, perm):
557 pass
558 pass
558
559
559 class sshv2protocolhandler(sshv1protocolhandler):
560 class sshv2protocolhandler(sshv1protocolhandler):
560 """Protocol handler for version 2 of the SSH protocol."""
561 """Protocol handler for version 2 of the SSH protocol."""
561
562
562 @property
563 @property
563 def name(self):
564 def name(self):
564 return wireprototypes.SSHV2
565 return wireprototypes.SSHV2
565
566
566 def addcapabilities(self, repo, caps):
567 def addcapabilities(self, repo, caps):
567 return caps
568 return caps
568
569
569 def _runsshserver(ui, repo, fin, fout, ev):
570 def _runsshserver(ui, repo, fin, fout, ev):
570 # This function operates like a state machine of sorts. The following
571 # This function operates like a state machine of sorts. The following
571 # states are defined:
572 # states are defined:
572 #
573 #
573 # protov1-serving
574 # protov1-serving
574 # Server is in protocol version 1 serving mode. Commands arrive on
575 # Server is in protocol version 1 serving mode. Commands arrive on
575 # new lines. These commands are processed in this state, one command
576 # new lines. These commands are processed in this state, one command
576 # after the other.
577 # after the other.
577 #
578 #
578 # protov2-serving
579 # protov2-serving
579 # Server is in protocol version 2 serving mode.
580 # Server is in protocol version 2 serving mode.
580 #
581 #
581 # upgrade-initial
582 # upgrade-initial
582 # The server is going to process an upgrade request.
583 # The server is going to process an upgrade request.
583 #
584 #
584 # upgrade-v2-filter-legacy-handshake
585 # upgrade-v2-filter-legacy-handshake
585 # The protocol is being upgraded to version 2. The server is expecting
586 # The protocol is being upgraded to version 2. The server is expecting
586 # the legacy handshake from version 1.
587 # the legacy handshake from version 1.
587 #
588 #
588 # upgrade-v2-finish
589 # upgrade-v2-finish
589 # The upgrade to version 2 of the protocol is imminent.
590 # The upgrade to version 2 of the protocol is imminent.
590 #
591 #
591 # shutdown
592 # shutdown
592 # The server is shutting down, possibly in reaction to a client event.
593 # The server is shutting down, possibly in reaction to a client event.
593 #
594 #
594 # And here are their transitions:
595 # And here are their transitions:
595 #
596 #
596 # protov1-serving -> shutdown
597 # protov1-serving -> shutdown
597 # When server receives an empty request or encounters another
598 # When server receives an empty request or encounters another
598 # error.
599 # error.
599 #
600 #
600 # protov1-serving -> upgrade-initial
601 # protov1-serving -> upgrade-initial
601 # An upgrade request line was seen.
602 # An upgrade request line was seen.
602 #
603 #
603 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
604 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
604 # Upgrade to version 2 in progress. Server is expecting to
605 # Upgrade to version 2 in progress. Server is expecting to
605 # process a legacy handshake.
606 # process a legacy handshake.
606 #
607 #
607 # upgrade-v2-filter-legacy-handshake -> shutdown
608 # upgrade-v2-filter-legacy-handshake -> shutdown
608 # Client did not fulfill upgrade handshake requirements.
609 # Client did not fulfill upgrade handshake requirements.
609 #
610 #
610 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
611 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
611 # Client fulfilled version 2 upgrade requirements. Finishing that
612 # Client fulfilled version 2 upgrade requirements. Finishing that
612 # upgrade.
613 # upgrade.
613 #
614 #
614 # upgrade-v2-finish -> protov2-serving
615 # upgrade-v2-finish -> protov2-serving
615 # Protocol upgrade to version 2 complete. Server can now speak protocol
616 # Protocol upgrade to version 2 complete. Server can now speak protocol
616 # version 2.
617 # version 2.
617 #
618 #
618 # protov2-serving -> protov1-serving
619 # protov2-serving -> protov1-serving
619 # Ths happens by default since protocol version 2 is the same as
620 # Ths happens by default since protocol version 2 is the same as
620 # version 1 except for the handshake.
621 # version 1 except for the handshake.
621
622
622 state = 'protov1-serving'
623 state = 'protov1-serving'
623 proto = sshv1protocolhandler(ui, fin, fout)
624 proto = sshv1protocolhandler(ui, fin, fout)
624 protoswitched = False
625 protoswitched = False
625
626
626 while not ev.is_set():
627 while not ev.is_set():
627 if state == 'protov1-serving':
628 if state == 'protov1-serving':
628 # Commands are issued on new lines.
629 # Commands are issued on new lines.
629 request = fin.readline()[:-1]
630 request = fin.readline()[:-1]
630
631
631 # Empty lines signal to terminate the connection.
632 # Empty lines signal to terminate the connection.
632 if not request:
633 if not request:
633 state = 'shutdown'
634 state = 'shutdown'
634 continue
635 continue
635
636
636 # It looks like a protocol upgrade request. Transition state to
637 # It looks like a protocol upgrade request. Transition state to
637 # handle it.
638 # handle it.
638 if request.startswith(b'upgrade '):
639 if request.startswith(b'upgrade '):
639 if protoswitched:
640 if protoswitched:
640 _sshv1respondooberror(fout, ui.ferr,
641 _sshv1respondooberror(fout, ui.ferr,
641 b'cannot upgrade protocols multiple '
642 b'cannot upgrade protocols multiple '
642 b'times')
643 b'times')
643 state = 'shutdown'
644 state = 'shutdown'
644 continue
645 continue
645
646
646 state = 'upgrade-initial'
647 state = 'upgrade-initial'
647 continue
648 continue
648
649
649 available = wireprotov1server.commands.commandavailable(
650 available = wireprotov1server.commands.commandavailable(
650 request, proto)
651 request, proto)
651
652
652 # This command isn't available. Send an empty response and go
653 # This command isn't available. Send an empty response and go
653 # back to waiting for a new command.
654 # back to waiting for a new command.
654 if not available:
655 if not available:
655 _sshv1respondbytes(fout, b'')
656 _sshv1respondbytes(fout, b'')
656 continue
657 continue
657
658
658 rsp = wireprotov1server.dispatch(repo, proto, request)
659 rsp = wireprotov1server.dispatch(repo, proto, request)
659
660
660 if isinstance(rsp, bytes):
661 if isinstance(rsp, bytes):
661 _sshv1respondbytes(fout, rsp)
662 _sshv1respondbytes(fout, rsp)
662 elif isinstance(rsp, wireprototypes.bytesresponse):
663 elif isinstance(rsp, wireprototypes.bytesresponse):
663 _sshv1respondbytes(fout, rsp.data)
664 _sshv1respondbytes(fout, rsp.data)
664 elif isinstance(rsp, wireprototypes.streamres):
665 elif isinstance(rsp, wireprototypes.streamres):
665 _sshv1respondstream(fout, rsp)
666 _sshv1respondstream(fout, rsp)
666 elif isinstance(rsp, wireprototypes.streamreslegacy):
667 elif isinstance(rsp, wireprototypes.streamreslegacy):
667 _sshv1respondstream(fout, rsp)
668 _sshv1respondstream(fout, rsp)
668 elif isinstance(rsp, wireprototypes.pushres):
669 elif isinstance(rsp, wireprototypes.pushres):
669 _sshv1respondbytes(fout, b'')
670 _sshv1respondbytes(fout, b'')
670 _sshv1respondbytes(fout, b'%d' % rsp.res)
671 _sshv1respondbytes(fout, b'%d' % rsp.res)
671 elif isinstance(rsp, wireprototypes.pusherr):
672 elif isinstance(rsp, wireprototypes.pusherr):
672 _sshv1respondbytes(fout, rsp.res)
673 _sshv1respondbytes(fout, rsp.res)
673 elif isinstance(rsp, wireprototypes.ooberror):
674 elif isinstance(rsp, wireprototypes.ooberror):
674 _sshv1respondooberror(fout, ui.ferr, rsp.message)
675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
675 else:
676 else:
676 raise error.ProgrammingError('unhandled response type from '
677 raise error.ProgrammingError('unhandled response type from '
677 'wire protocol command: %s' % rsp)
678 'wire protocol command: %s' % rsp)
678
679
679 # For now, protocol version 2 serving just goes back to version 1.
680 # For now, protocol version 2 serving just goes back to version 1.
680 elif state == 'protov2-serving':
681 elif state == 'protov2-serving':
681 state = 'protov1-serving'
682 state = 'protov1-serving'
682 continue
683 continue
683
684
684 elif state == 'upgrade-initial':
685 elif state == 'upgrade-initial':
685 # We should never transition into this state if we've switched
686 # We should never transition into this state if we've switched
686 # protocols.
687 # protocols.
687 assert not protoswitched
688 assert not protoswitched
688 assert proto.name == wireprototypes.SSHV1
689 assert proto.name == wireprototypes.SSHV1
689
690
690 # Expected: upgrade <token> <capabilities>
691 # Expected: upgrade <token> <capabilities>
691 # If we get something else, the request is malformed. It could be
692 # If we get something else, the request is malformed. It could be
692 # from a future client that has altered the upgrade line content.
693 # from a future client that has altered the upgrade line content.
693 # We treat this as an unknown command.
694 # We treat this as an unknown command.
694 try:
695 try:
695 token, caps = request.split(b' ')[1:]
696 token, caps = request.split(b' ')[1:]
696 except ValueError:
697 except ValueError:
697 _sshv1respondbytes(fout, b'')
698 _sshv1respondbytes(fout, b'')
698 state = 'protov1-serving'
699 state = 'protov1-serving'
699 continue
700 continue
700
701
701 # Send empty response if we don't support upgrading protocols.
702 # Send empty response if we don't support upgrading protocols.
702 if not ui.configbool('experimental', 'sshserver.support-v2'):
703 if not ui.configbool('experimental', 'sshserver.support-v2'):
703 _sshv1respondbytes(fout, b'')
704 _sshv1respondbytes(fout, b'')
704 state = 'protov1-serving'
705 state = 'protov1-serving'
705 continue
706 continue
706
707
707 try:
708 try:
708 caps = urlreq.parseqs(caps)
709 caps = urlreq.parseqs(caps)
709 except ValueError:
710 except ValueError:
710 _sshv1respondbytes(fout, b'')
711 _sshv1respondbytes(fout, b'')
711 state = 'protov1-serving'
712 state = 'protov1-serving'
712 continue
713 continue
713
714
714 # We don't see an upgrade request to protocol version 2. Ignore
715 # We don't see an upgrade request to protocol version 2. Ignore
715 # the upgrade request.
716 # the upgrade request.
716 wantedprotos = caps.get(b'proto', [b''])[0]
717 wantedprotos = caps.get(b'proto', [b''])[0]
717 if SSHV2 not in wantedprotos:
718 if SSHV2 not in wantedprotos:
718 _sshv1respondbytes(fout, b'')
719 _sshv1respondbytes(fout, b'')
719 state = 'protov1-serving'
720 state = 'protov1-serving'
720 continue
721 continue
721
722
722 # It looks like we can honor this upgrade request to protocol 2.
723 # It looks like we can honor this upgrade request to protocol 2.
723 # Filter the rest of the handshake protocol request lines.
724 # Filter the rest of the handshake protocol request lines.
724 state = 'upgrade-v2-filter-legacy-handshake'
725 state = 'upgrade-v2-filter-legacy-handshake'
725 continue
726 continue
726
727
727 elif state == 'upgrade-v2-filter-legacy-handshake':
728 elif state == 'upgrade-v2-filter-legacy-handshake':
728 # Client should have sent legacy handshake after an ``upgrade``
729 # Client should have sent legacy handshake after an ``upgrade``
729 # request. Expected lines:
730 # request. Expected lines:
730 #
731 #
731 # hello
732 # hello
732 # between
733 # between
733 # pairs 81
734 # pairs 81
734 # 0000...-0000...
735 # 0000...-0000...
735
736
736 ok = True
737 ok = True
737 for line in (b'hello', b'between', b'pairs 81'):
738 for line in (b'hello', b'between', b'pairs 81'):
738 request = fin.readline()[:-1]
739 request = fin.readline()[:-1]
739
740
740 if request != line:
741 if request != line:
741 _sshv1respondooberror(fout, ui.ferr,
742 _sshv1respondooberror(fout, ui.ferr,
742 b'malformed handshake protocol: '
743 b'malformed handshake protocol: '
743 b'missing %s' % line)
744 b'missing %s' % line)
744 ok = False
745 ok = False
745 state = 'shutdown'
746 state = 'shutdown'
746 break
747 break
747
748
748 if not ok:
749 if not ok:
749 continue
750 continue
750
751
751 request = fin.read(81)
752 request = fin.read(81)
752 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
753 _sshv1respondooberror(fout, ui.ferr,
754 _sshv1respondooberror(fout, ui.ferr,
754 b'malformed handshake protocol: '
755 b'malformed handshake protocol: '
755 b'missing between argument value')
756 b'missing between argument value')
756 state = 'shutdown'
757 state = 'shutdown'
757 continue
758 continue
758
759
759 state = 'upgrade-v2-finish'
760 state = 'upgrade-v2-finish'
760 continue
761 continue
761
762
762 elif state == 'upgrade-v2-finish':
763 elif state == 'upgrade-v2-finish':
763 # Send the upgrade response.
764 # Send the upgrade response.
764 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
765 servercaps = wireprotov1server.capabilities(repo, proto)
766 servercaps = wireprotov1server.capabilities(repo, proto)
766 rsp = b'capabilities: %s' % servercaps.data
767 rsp = b'capabilities: %s' % servercaps.data
767 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
768 fout.flush()
769 fout.flush()
769
770
770 proto = sshv2protocolhandler(ui, fin, fout)
771 proto = sshv2protocolhandler(ui, fin, fout)
771 protoswitched = True
772 protoswitched = True
772
773
773 state = 'protov2-serving'
774 state = 'protov2-serving'
774 continue
775 continue
775
776
776 elif state == 'shutdown':
777 elif state == 'shutdown':
777 break
778 break
778
779
779 else:
780 else:
780 raise error.ProgrammingError('unhandled ssh server state: %s' %
781 raise error.ProgrammingError('unhandled ssh server state: %s' %
781 state)
782 state)
782
783
783 class sshserver(object):
784 class sshserver(object):
784 def __init__(self, ui, repo, logfh=None):
785 def __init__(self, ui, repo, logfh=None):
785 self._ui = ui
786 self._ui = ui
786 self._repo = repo
787 self._repo = repo
787 self._fin, self._fout = procutil.protectstdio(ui.fin, ui.fout)
788 self._fin, self._fout = procutil.protectstdio(ui.fin, ui.fout)
788 # TODO: manage the redirection flag internally by ui
789 # TODO: manage the redirection flag internally by ui
789 ui._finoutredirected = (self._fin, self._fout) != (ui.fin, ui.fout)
790 ui._finoutredirected = (self._fin, self._fout) != (ui.fin, ui.fout)
790
791
791 # Log write I/O to stdout and stderr if configured.
792 # Log write I/O to stdout and stderr if configured.
792 if logfh:
793 if logfh:
793 self._fout = util.makeloggingfileobject(
794 self._fout = util.makeloggingfileobject(
794 logfh, self._fout, 'o', logdata=True)
795 logfh, self._fout, 'o', logdata=True)
795 ui.ferr = util.makeloggingfileobject(
796 ui.ferr = util.makeloggingfileobject(
796 logfh, ui.ferr, 'e', logdata=True)
797 logfh, ui.ferr, 'e', logdata=True)
797
798
798 def serve_forever(self):
799 def serve_forever(self):
799 self.serveuntil(threading.Event())
800 self.serveuntil(threading.Event())
800 procutil.restorestdio(self._ui.fin, self._ui.fout,
801 procutil.restorestdio(self._ui.fin, self._ui.fout,
801 self._fin, self._fout)
802 self._fin, self._fout)
802 sys.exit(0)
803 sys.exit(0)
803
804
804 def serveuntil(self, ev):
805 def serveuntil(self, ev):
805 """Serve until a threading.Event is set."""
806 """Serve until a threading.Event is set."""
806 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
807 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now