##// END OF EJS Templates
configitems: register the 'web.description' config
Boris Feld -
r34235:a6c18628 default
parent child Browse files
Show More
@@ -1,215 +1,219 b''
1 1 # zeroconf.py - zeroconf support for Mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 '''discover and advertise repositories on the local network
8 8
9 9 Zeroconf-enabled repositories will be announced in a network without
10 10 the need to configure a server or a service. They can be discovered
11 11 without knowing their actual IP address.
12 12
13 13 To allow other people to discover your repository using run
14 14 :hg:`serve` in your repository::
15 15
16 16 $ cd test
17 17 $ hg serve
18 18
19 19 You can discover Zeroconf-enabled repositories by running
20 20 :hg:`paths`::
21 21
22 22 $ hg paths
23 23 zc-test = http://example.com:8000/test
24 24 '''
25 25 from __future__ import absolute_import
26 26
27 27 import os
28 28 import socket
29 29 import time
30 30
31 31 from . import Zeroconf
32 32 from mercurial import (
33 33 dispatch,
34 34 encoding,
35 35 extensions,
36 36 hg,
37 37 ui as uimod,
38 38 )
39 39 from mercurial.hgweb import (
40 40 server as servermod
41 41 )
42 42
43 43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
44 44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 45 # be specifying the version(s) of Mercurial they are tested with, or
46 46 # leave the attribute unspecified.
47 47 testedwith = 'ships-with-hg-core'
48 48
49 49 # publish
50 50
51 51 server = None
52 52 localip = None
53 53
54 54 def getip():
55 55 # finds external-facing interface without sending any packets (Linux)
56 56 try:
57 57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
58 58 s.connect(('1.0.0.1', 0))
59 59 ip = s.getsockname()[0]
60 60 return ip
61 61 except socket.error:
62 62 pass
63 63
64 64 # Generic method, sometimes gives useless results
65 65 try:
66 66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
67 67 if ':' in dumbip:
68 68 dumbip = '127.0.0.1'
69 69 if not dumbip.startswith('127.'):
70 70 return dumbip
71 71 except (socket.gaierror, socket.herror):
72 72 dumbip = '127.0.0.1'
73 73
74 74 # works elsewhere, but actually sends a packet
75 75 try:
76 76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77 77 s.connect(('1.0.0.1', 1))
78 78 ip = s.getsockname()[0]
79 79 return ip
80 80 except socket.error:
81 81 pass
82 82
83 83 return dumbip
84 84
85 85 def publish(name, desc, path, port):
86 86 global server, localip
87 87 if not server:
88 88 ip = getip()
89 89 if ip.startswith('127.'):
90 90 # if we have no internet connection, this can happen.
91 91 return
92 92 localip = socket.inet_aton(ip)
93 93 server = Zeroconf.Zeroconf(ip)
94 94
95 95 hostname = socket.gethostname().split('.')[0]
96 96 host = hostname + ".local"
97 97 name = "%s-%s" % (hostname, name)
98 98
99 99 # advertise to browsers
100 100 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
101 101 name + '._http._tcp.local.',
102 102 server = host,
103 103 port = port,
104 104 properties = {'description': desc,
105 105 'path': "/" + path},
106 106 address = localip, weight = 0, priority = 0)
107 107 server.registerService(svc)
108 108
109 109 # advertise to Mercurial clients
110 110 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
111 111 name + '._hg._tcp.local.',
112 112 server = host,
113 113 port = port,
114 114 properties = {'description': desc,
115 115 'path': "/" + path},
116 116 address = localip, weight = 0, priority = 0)
117 117 server.registerService(svc)
118 118
119 119 def zc_create_server(create_server, ui, app):
120 120 httpd = create_server(ui, app)
121 121 port = httpd.port
122 122
123 123 try:
124 124 repos = app.repos
125 125 except AttributeError:
126 126 # single repo
127 127 with app._obtainrepo() as repo:
128 128 name = app.reponame or os.path.basename(repo.root)
129 129 path = repo.ui.config("web", "prefix", "").strip('/')
130 desc = repo.ui.config("web", "description", name)
130 desc = repo.ui.config("web", "description")
131 if not desc:
132 desc = name
131 133 publish(name, desc, path, port)
132 134 else:
133 135 # webdir
134 136 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
135 137 for repo, path in repos:
136 138 u = app.ui.copy()
137 139 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
138 140 name = os.path.basename(repo)
139 141 path = (prefix + repo).strip('/')
140 desc = u.config('web', 'description', name)
142 desc = u.config('web', 'description')
143 if not desc:
144 desc = name
141 145 publish(name, desc, path, port)
142 146 return httpd
143 147
144 148 # listen
145 149
146 150 class listener(object):
147 151 def __init__(self):
148 152 self.found = {}
149 153 def removeService(self, server, type, name):
150 154 if repr(name) in self.found:
151 155 del self.found[repr(name)]
152 156 def addService(self, server, type, name):
153 157 self.found[repr(name)] = server.getServiceInfo(type, name)
154 158
155 159 def getzcpaths():
156 160 ip = getip()
157 161 if ip.startswith('127.'):
158 162 return
159 163 server = Zeroconf.Zeroconf(ip)
160 164 l = listener()
161 165 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
162 166 time.sleep(1)
163 167 server.close()
164 168 for value in l.found.values():
165 169 name = value.name[:value.name.index('.')]
166 170 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
167 171 value.properties.get("path", "/"))
168 172 yield "zc-" + name, url
169 173
170 174 def config(orig, self, section, key, *args, **kwargs):
171 175 if section == "paths" and key.startswith("zc-"):
172 176 for name, path in getzcpaths():
173 177 if name == key:
174 178 return path
175 179 return orig(self, section, key, *args, **kwargs)
176 180
177 181 def configitems(orig, self, section, *args, **kwargs):
178 182 repos = orig(self, section, *args, **kwargs)
179 183 if section == "paths":
180 184 repos += getzcpaths()
181 185 return repos
182 186
183 187 def configsuboptions(orig, self, section, name, *args, **kwargs):
184 188 opt, sub = orig(self, section, name, *args, **kwargs)
185 189 if section == "paths" and name.startswith("zc-"):
186 190 # We have to find the URL in the zeroconf paths. We can't cons up any
187 191 # suboptions, so we use any that we found in the original config.
188 192 for zcname, zcurl in getzcpaths():
189 193 if zcname == name:
190 194 return zcurl, sub
191 195 return opt, sub
192 196
193 197 def defaultdest(orig, source):
194 198 for name, path in getzcpaths():
195 199 if path == source:
196 200 return name.encode(encoding.encoding)
197 201 return orig(source)
198 202
199 203 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
200 204 try:
201 205 return orig(ui, options, cmd, cmdfunc)
202 206 finally:
203 207 # we need to call close() on the server to notify() the various
204 208 # threading Conditions and allow the background threads to exit
205 209 global server
206 210 if server:
207 211 server.close()
208 212
209 213 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
210 214
211 215 extensions.wrapfunction(uimod.ui, 'config', config)
212 216 extensions.wrapfunction(uimod.ui, 'configitems', configitems)
213 217 extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions)
214 218 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
215 219 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
@@ -1,634 +1,637 b''
1 1 # configitems.py - centralized declaration of configuration option
2 2 #
3 3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import functools
11 11
12 12 from . import (
13 13 error,
14 14 )
15 15
16 16 def loadconfigtable(ui, extname, configtable):
17 17 """update config item known to the ui with the extension ones"""
18 18 for section, items in configtable.items():
19 19 knownitems = ui._knownconfig.setdefault(section, {})
20 20 knownkeys = set(knownitems)
21 21 newkeys = set(items)
22 22 for key in sorted(knownkeys & newkeys):
23 23 msg = "extension '%s' overwrite config item '%s.%s'"
24 24 msg %= (extname, section, key)
25 25 ui.develwarn(msg, config='warn-config')
26 26
27 27 knownitems.update(items)
28 28
29 29 class configitem(object):
30 30 """represent a known config item
31 31
32 32 :section: the official config section where to find this item,
33 33 :name: the official name within the section,
34 34 :default: default value for this item,
35 35 :alias: optional list of tuples as alternatives.
36 36 """
37 37
38 38 def __init__(self, section, name, default=None, alias=()):
39 39 self.section = section
40 40 self.name = name
41 41 self.default = default
42 42 self.alias = list(alias)
43 43
44 44 coreitems = {}
45 45
46 46 def _register(configtable, *args, **kwargs):
47 47 item = configitem(*args, **kwargs)
48 48 section = configtable.setdefault(item.section, {})
49 49 if item.name in section:
50 50 msg = "duplicated config item registration for '%s.%s'"
51 51 raise error.ProgrammingError(msg % (item.section, item.name))
52 52 section[item.name] = item
53 53
54 54 # special value for case where the default is derived from other values
55 55 dynamicdefault = object()
56 56
57 57 # Registering actual config items
58 58
59 59 def getitemregister(configtable):
60 60 return functools.partial(_register, configtable)
61 61
62 62 coreconfigitem = getitemregister(coreitems)
63 63
64 64 coreconfigitem('auth', 'cookiefile',
65 65 default=None,
66 66 )
67 67 # bookmarks.pushing: internal hack for discovery
68 68 coreconfigitem('bookmarks', 'pushing',
69 69 default=list,
70 70 )
71 71 # bundle.mainreporoot: internal hack for bundlerepo
72 72 coreconfigitem('bundle', 'mainreporoot',
73 73 default='',
74 74 )
75 75 # bundle.reorder: experimental config
76 76 coreconfigitem('bundle', 'reorder',
77 77 default='auto',
78 78 )
79 79 coreconfigitem('censor', 'policy',
80 80 default='abort',
81 81 )
82 82 coreconfigitem('chgserver', 'idletimeout',
83 83 default=3600,
84 84 )
85 85 coreconfigitem('chgserver', 'skiphash',
86 86 default=False,
87 87 )
88 88 coreconfigitem('cmdserver', 'log',
89 89 default=None,
90 90 )
91 91 coreconfigitem('color', 'mode',
92 92 default='auto',
93 93 )
94 94 coreconfigitem('color', 'pagermode',
95 95 default=dynamicdefault,
96 96 )
97 97 coreconfigitem('commands', 'status.relative',
98 98 default=False,
99 99 )
100 100 coreconfigitem('commands', 'status.skipstates',
101 101 default=[],
102 102 )
103 103 coreconfigitem('commands', 'status.verbose',
104 104 default=False,
105 105 )
106 106 coreconfigitem('commands', 'update.requiredest',
107 107 default=False,
108 108 )
109 109 coreconfigitem('devel', 'all-warnings',
110 110 default=False,
111 111 )
112 112 coreconfigitem('devel', 'bundle2.debug',
113 113 default=False,
114 114 )
115 115 coreconfigitem('devel', 'check-locks',
116 116 default=False,
117 117 )
118 118 coreconfigitem('devel', 'check-relroot',
119 119 default=False,
120 120 )
121 121 coreconfigitem('devel', 'default-date',
122 122 default=None,
123 123 )
124 124 coreconfigitem('devel', 'deprec-warn',
125 125 default=False,
126 126 )
127 127 coreconfigitem('devel', 'disableloaddefaultcerts',
128 128 default=False,
129 129 )
130 130 coreconfigitem('devel', 'legacy.exchange',
131 131 default=list,
132 132 )
133 133 coreconfigitem('devel', 'servercafile',
134 134 default='',
135 135 )
136 136 coreconfigitem('devel', 'serverexactprotocol',
137 137 default='',
138 138 )
139 139 coreconfigitem('devel', 'serverrequirecert',
140 140 default=False,
141 141 )
142 142 coreconfigitem('devel', 'strip-obsmarkers',
143 143 default=True,
144 144 )
145 145 coreconfigitem('email', 'charsets',
146 146 default=list,
147 147 )
148 148 coreconfigitem('email', 'method',
149 149 default='smtp',
150 150 )
151 151 coreconfigitem('experimental', 'bundle-phases',
152 152 default=False,
153 153 )
154 154 coreconfigitem('experimental', 'bundle2-advertise',
155 155 default=True,
156 156 )
157 157 coreconfigitem('experimental', 'bundle2-output-capture',
158 158 default=False,
159 159 )
160 160 coreconfigitem('experimental', 'bundle2.pushback',
161 161 default=False,
162 162 )
163 163 coreconfigitem('experimental', 'bundle2lazylocking',
164 164 default=False,
165 165 )
166 166 coreconfigitem('experimental', 'bundlecomplevel',
167 167 default=None,
168 168 )
169 169 coreconfigitem('experimental', 'changegroup3',
170 170 default=False,
171 171 )
172 172 coreconfigitem('experimental', 'clientcompressionengines',
173 173 default=list,
174 174 )
175 175 coreconfigitem('experimental', 'copytrace',
176 176 default='on',
177 177 )
178 178 coreconfigitem('experimental', 'crecordtest',
179 179 default=None,
180 180 )
181 181 coreconfigitem('experimental', 'editortmpinhg',
182 182 default=False,
183 183 )
184 184 coreconfigitem('experimental', 'stabilization',
185 185 default=list,
186 186 alias=[('experimental', 'evolution')],
187 187 )
188 188 coreconfigitem('experimental', 'stabilization.bundle-obsmarker',
189 189 default=False,
190 190 alias=[('experimental', 'evolution.bundle-obsmarker')],
191 191 )
192 192 coreconfigitem('experimental', 'stabilization.track-operation',
193 193 default=False,
194 194 alias=[('experimental', 'evolution.track-operation')]
195 195 )
196 196 coreconfigitem('experimental', 'exportableenviron',
197 197 default=list,
198 198 )
199 199 coreconfigitem('experimental', 'extendedheader.index',
200 200 default=None,
201 201 )
202 202 coreconfigitem('experimental', 'extendedheader.similarity',
203 203 default=False,
204 204 )
205 205 coreconfigitem('experimental', 'format.compression',
206 206 default='zlib',
207 207 )
208 208 coreconfigitem('experimental', 'graphshorten',
209 209 default=False,
210 210 )
211 211 coreconfigitem('experimental', 'hook-track-tags',
212 212 default=False,
213 213 )
214 214 coreconfigitem('experimental', 'httppostargs',
215 215 default=False,
216 216 )
217 217 coreconfigitem('experimental', 'manifestv2',
218 218 default=False,
219 219 )
220 220 coreconfigitem('experimental', 'mergedriver',
221 221 default=None,
222 222 )
223 223 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
224 224 default=False,
225 225 )
226 226 coreconfigitem('experimental', 'rebase.multidest',
227 227 default=False,
228 228 )
229 229 coreconfigitem('experimental', 'revertalternateinteractivemode',
230 230 default=True,
231 231 )
232 232 coreconfigitem('experimental', 'revlogv2',
233 233 default=None,
234 234 )
235 235 coreconfigitem('experimental', 'spacemovesdown',
236 236 default=False,
237 237 )
238 238 coreconfigitem('experimental', 'treemanifest',
239 239 default=False,
240 240 )
241 241 coreconfigitem('experimental', 'updatecheck',
242 242 default=None,
243 243 )
244 244 coreconfigitem('format', 'aggressivemergedeltas',
245 245 default=False,
246 246 )
247 247 coreconfigitem('format', 'chunkcachesize',
248 248 default=None,
249 249 )
250 250 coreconfigitem('format', 'dotencode',
251 251 default=True,
252 252 )
253 253 coreconfigitem('format', 'generaldelta',
254 254 default=False,
255 255 )
256 256 coreconfigitem('format', 'manifestcachesize',
257 257 default=None,
258 258 )
259 259 coreconfigitem('format', 'maxchainlen',
260 260 default=None,
261 261 )
262 262 coreconfigitem('format', 'obsstore-version',
263 263 default=None,
264 264 )
265 265 coreconfigitem('format', 'usefncache',
266 266 default=True,
267 267 )
268 268 coreconfigitem('format', 'usegeneraldelta',
269 269 default=True,
270 270 )
271 271 coreconfigitem('format', 'usestore',
272 272 default=True,
273 273 )
274 274 coreconfigitem('hostsecurity', 'ciphers',
275 275 default=None,
276 276 )
277 277 coreconfigitem('hostsecurity', 'disabletls10warning',
278 278 default=False,
279 279 )
280 280 coreconfigitem('http_proxy', 'always',
281 281 default=False,
282 282 )
283 283 coreconfigitem('http_proxy', 'host',
284 284 default=None,
285 285 )
286 286 coreconfigitem('http_proxy', 'no',
287 287 default=list,
288 288 )
289 289 coreconfigitem('http_proxy', 'passwd',
290 290 default=None,
291 291 )
292 292 coreconfigitem('http_proxy', 'user',
293 293 default=None,
294 294 )
295 295 coreconfigitem('merge', 'followcopies',
296 296 default=True,
297 297 )
298 298 coreconfigitem('pager', 'ignore',
299 299 default=list,
300 300 )
301 301 coreconfigitem('patch', 'eol',
302 302 default='strict',
303 303 )
304 304 coreconfigitem('patch', 'fuzz',
305 305 default=2,
306 306 )
307 307 coreconfigitem('paths', 'default',
308 308 default=None,
309 309 )
310 310 coreconfigitem('paths', 'default-push',
311 311 default=None,
312 312 )
313 313 coreconfigitem('phases', 'checksubrepos',
314 314 default='follow',
315 315 )
316 316 coreconfigitem('phases', 'publish',
317 317 default=True,
318 318 )
319 319 coreconfigitem('profiling', 'enabled',
320 320 default=False,
321 321 )
322 322 coreconfigitem('profiling', 'format',
323 323 default='text',
324 324 )
325 325 coreconfigitem('profiling', 'freq',
326 326 default=1000,
327 327 )
328 328 coreconfigitem('profiling', 'limit',
329 329 default=30,
330 330 )
331 331 coreconfigitem('profiling', 'nested',
332 332 default=0,
333 333 )
334 334 coreconfigitem('profiling', 'sort',
335 335 default='inlinetime',
336 336 )
337 337 coreconfigitem('profiling', 'statformat',
338 338 default='hotpath',
339 339 )
340 340 coreconfigitem('progress', 'assume-tty',
341 341 default=False,
342 342 )
343 343 coreconfigitem('progress', 'changedelay',
344 344 default=1,
345 345 )
346 346 coreconfigitem('progress', 'clear-complete',
347 347 default=True,
348 348 )
349 349 coreconfigitem('progress', 'debug',
350 350 default=False,
351 351 )
352 352 coreconfigitem('progress', 'delay',
353 353 default=3,
354 354 )
355 355 coreconfigitem('progress', 'disable',
356 356 default=False,
357 357 )
358 358 coreconfigitem('progress', 'estimate',
359 359 default=2,
360 360 )
361 361 coreconfigitem('progress', 'refresh',
362 362 default=0.1,
363 363 )
364 364 coreconfigitem('progress', 'width',
365 365 default=dynamicdefault,
366 366 )
367 367 coreconfigitem('push', 'pushvars.server',
368 368 default=False,
369 369 )
370 370 coreconfigitem('server', 'bundle1',
371 371 default=True,
372 372 )
373 373 coreconfigitem('server', 'bundle1gd',
374 374 default=None,
375 375 )
376 376 coreconfigitem('server', 'compressionengines',
377 377 default=list,
378 378 )
379 379 coreconfigitem('server', 'concurrent-push-mode',
380 380 default='strict',
381 381 )
382 382 coreconfigitem('server', 'disablefullbundle',
383 383 default=False,
384 384 )
385 385 coreconfigitem('server', 'maxhttpheaderlen',
386 386 default=1024,
387 387 )
388 388 coreconfigitem('server', 'preferuncompressed',
389 389 default=False,
390 390 )
391 391 coreconfigitem('server', 'uncompressed',
392 392 default=True,
393 393 )
394 394 coreconfigitem('server', 'uncompressedallowsecret',
395 395 default=False,
396 396 )
397 397 coreconfigitem('server', 'validate',
398 398 default=False,
399 399 )
400 400 coreconfigitem('server', 'zliblevel',
401 401 default=-1,
402 402 )
403 403 coreconfigitem('smtp', 'host',
404 404 default=None,
405 405 )
406 406 coreconfigitem('smtp', 'local_hostname',
407 407 default=None,
408 408 )
409 409 coreconfigitem('smtp', 'password',
410 410 default=None,
411 411 )
412 412 coreconfigitem('smtp', 'tls',
413 413 default='none',
414 414 )
415 415 coreconfigitem('smtp', 'username',
416 416 default=None,
417 417 )
418 418 coreconfigitem('sparse', 'missingwarning',
419 419 default=True,
420 420 )
421 421 coreconfigitem('trusted', 'groups',
422 422 default=list,
423 423 )
424 424 coreconfigitem('trusted', 'users',
425 425 default=list,
426 426 )
427 427 coreconfigitem('ui', '_usedassubrepo',
428 428 default=False,
429 429 )
430 430 coreconfigitem('ui', 'allowemptycommit',
431 431 default=False,
432 432 )
433 433 coreconfigitem('ui', 'archivemeta',
434 434 default=True,
435 435 )
436 436 coreconfigitem('ui', 'askusername',
437 437 default=False,
438 438 )
439 439 coreconfigitem('ui', 'clonebundlefallback',
440 440 default=False,
441 441 )
442 442 coreconfigitem('ui', 'clonebundleprefers',
443 443 default=list,
444 444 )
445 445 coreconfigitem('ui', 'clonebundles',
446 446 default=True,
447 447 )
448 448 coreconfigitem('ui', 'color',
449 449 default='auto',
450 450 )
451 451 coreconfigitem('ui', 'commitsubrepos',
452 452 default=False,
453 453 )
454 454 coreconfigitem('ui', 'debug',
455 455 default=False,
456 456 )
457 457 coreconfigitem('ui', 'debugger',
458 458 default=None,
459 459 )
460 460 coreconfigitem('ui', 'fallbackencoding',
461 461 default=None,
462 462 )
463 463 coreconfigitem('ui', 'forcecwd',
464 464 default=None,
465 465 )
466 466 coreconfigitem('ui', 'forcemerge',
467 467 default=None,
468 468 )
469 469 coreconfigitem('ui', 'formatdebug',
470 470 default=False,
471 471 )
472 472 coreconfigitem('ui', 'formatjson',
473 473 default=False,
474 474 )
475 475 coreconfigitem('ui', 'formatted',
476 476 default=None,
477 477 )
478 478 coreconfigitem('ui', 'graphnodetemplate',
479 479 default=None,
480 480 )
481 481 coreconfigitem('ui', 'http2debuglevel',
482 482 default=None,
483 483 )
484 484 coreconfigitem('ui', 'interactive',
485 485 default=None,
486 486 )
487 487 coreconfigitem('ui', 'interface',
488 488 default=None,
489 489 )
490 490 coreconfigitem('ui', 'logblockedtimes',
491 491 default=False,
492 492 )
493 493 coreconfigitem('ui', 'logtemplate',
494 494 default=None,
495 495 )
496 496 coreconfigitem('ui', 'merge',
497 497 default=None,
498 498 )
499 499 coreconfigitem('ui', 'mergemarkers',
500 500 default='basic',
501 501 )
502 502 coreconfigitem('ui', 'mergemarkertemplate',
503 503 default=('{node|short} '
504 504 '{ifeq(tags, "tip", "", '
505 505 'ifeq(tags, "", "", "{tags} "))}'
506 506 '{if(bookmarks, "{bookmarks} ")}'
507 507 '{ifeq(branch, "default", "", "{branch} ")}'
508 508 '- {author|user}: {desc|firstline}')
509 509 )
510 510 coreconfigitem('ui', 'nontty',
511 511 default=False,
512 512 )
513 513 coreconfigitem('ui', 'origbackuppath',
514 514 default=None,
515 515 )
516 516 coreconfigitem('ui', 'paginate',
517 517 default=True,
518 518 )
519 519 coreconfigitem('ui', 'patch',
520 520 default=None,
521 521 )
522 522 coreconfigitem('ui', 'portablefilenames',
523 523 default='warn',
524 524 )
525 525 coreconfigitem('ui', 'promptecho',
526 526 default=False,
527 527 )
528 528 coreconfigitem('ui', 'quiet',
529 529 default=False,
530 530 )
531 531 coreconfigitem('ui', 'quietbookmarkmove',
532 532 default=False,
533 533 )
534 534 coreconfigitem('ui', 'remotecmd',
535 535 default='hg',
536 536 )
537 537 coreconfigitem('ui', 'report_untrusted',
538 538 default=True,
539 539 )
540 540 coreconfigitem('ui', 'rollback',
541 541 default=True,
542 542 )
543 543 coreconfigitem('ui', 'slash',
544 544 default=False,
545 545 )
546 546 coreconfigitem('ui', 'ssh',
547 547 default='ssh',
548 548 )
549 549 coreconfigitem('ui', 'statuscopies',
550 550 default=False,
551 551 )
552 552 coreconfigitem('ui', 'strict',
553 553 default=False,
554 554 )
555 555 coreconfigitem('ui', 'style',
556 556 default='',
557 557 )
558 558 coreconfigitem('ui', 'supportcontact',
559 559 default=None,
560 560 )
561 561 coreconfigitem('ui', 'textwidth',
562 562 default=78,
563 563 )
564 564 coreconfigitem('ui', 'timeout',
565 565 default='600',
566 566 )
567 567 coreconfigitem('ui', 'traceback',
568 568 default=False,
569 569 )
570 570 coreconfigitem('ui', 'tweakdefaults',
571 571 default=False,
572 572 )
573 573 coreconfigitem('ui', 'usehttp2',
574 574 default=False,
575 575 )
576 576 coreconfigitem('ui', 'username',
577 577 alias=[('ui', 'user')]
578 578 )
579 579 coreconfigitem('ui', 'verbose',
580 580 default=False,
581 581 )
582 582 coreconfigitem('verify', 'skipflags',
583 583 default=None,
584 584 )
585 585 coreconfigitem('web', 'accesslog',
586 586 default='-',
587 587 )
588 588 coreconfigitem('web', 'address',
589 589 default='',
590 590 )
591 591 coreconfigitem('web', 'allow_archive',
592 592 default=list,
593 593 )
594 594 coreconfigitem('web', 'allow_read',
595 595 default=list,
596 596 )
597 597 coreconfigitem('web', 'baseurl',
598 598 default=None,
599 599 )
600 600 coreconfigitem('web', 'cacerts',
601 601 default=None,
602 602 )
603 603 coreconfigitem('web', 'certificate',
604 604 default=None,
605 605 )
606 606 coreconfigitem('web', 'collapse',
607 607 default=False,
608 608 )
609 609 coreconfigitem('web', 'csp',
610 610 default=None,
611 611 )
612 612 coreconfigitem('web', 'deny_read',
613 613 default=list,
614 614 )
615 615 coreconfigitem('web', 'descend',
616 616 default=True,
617 617 )
618 coreconfigitem('web', 'description',
619 default="",
620 )
618 621 coreconfigitem('worker', 'backgroundclose',
619 622 default=dynamicdefault,
620 623 )
621 624 # Windows defaults to a limit of 512 open files. A buffer of 128
622 625 # should give us enough headway.
623 626 coreconfigitem('worker', 'backgroundclosemaxqueue',
624 627 default=384,
625 628 )
626 629 coreconfigitem('worker', 'backgroundcloseminfilecount',
627 630 default=2048,
628 631 )
629 632 coreconfigitem('worker', 'backgroundclosethreadcount',
630 633 default=4,
631 634 )
632 635 coreconfigitem('worker', 'numcpus',
633 636 default=None,
634 637 )
@@ -1,540 +1,540 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 cspvalues,
23 23 get_contact,
24 24 get_mtime,
25 25 ismember,
26 26 paritygen,
27 27 staticfile,
28 28 )
29 29 from .request import wsgirequest
30 30
31 31 from .. import (
32 32 encoding,
33 33 error,
34 34 hg,
35 35 profiling,
36 36 scmutil,
37 37 templater,
38 38 ui as uimod,
39 39 util,
40 40 )
41 41
42 42 from . import (
43 43 hgweb_mod,
44 44 webutil,
45 45 wsgicgi,
46 46 )
47 47
48 48 def cleannames(items):
49 49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
50 50
51 51 def findrepos(paths):
52 52 repos = []
53 53 for prefix, root in cleannames(paths):
54 54 roothead, roottail = os.path.split(root)
55 55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
56 56 # /bar/ be served as as foo/N .
57 57 # '*' will not search inside dirs with .hg (except .hg/patches),
58 58 # '**' will search inside dirs with .hg (and thus also find subrepos).
59 59 try:
60 60 recurse = {'*': False, '**': True}[roottail]
61 61 except KeyError:
62 62 repos.append((prefix, root))
63 63 continue
64 64 roothead = os.path.normpath(os.path.abspath(roothead))
65 65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
66 66 repos.extend(urlrepos(prefix, roothead, paths))
67 67 return repos
68 68
69 69 def urlrepos(prefix, roothead, paths):
70 70 """yield url paths and filesystem paths from a list of repo paths
71 71
72 72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
73 73 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
74 74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
75 75 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
77 77 """
78 78 for path in paths:
79 79 path = os.path.normpath(path)
80 80 yield (prefix + '/' +
81 81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
82 82
83 83 def geturlcgivars(baseurl, port):
84 84 """
85 85 Extract CGI variables from baseurl
86 86
87 87 >>> geturlcgivars(b"http://host.org/base", b"80")
88 88 ('host.org', '80', '/base')
89 89 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
90 90 ('host.org', '8000', '/base')
91 91 >>> geturlcgivars(b'/base', 8000)
92 92 ('', '8000', '/base')
93 93 >>> geturlcgivars(b"base", b'8000')
94 94 ('', '8000', '/base')
95 95 >>> geturlcgivars(b"http://host", b'8000')
96 96 ('host', '8000', '/')
97 97 >>> geturlcgivars(b"http://host/", b'8000')
98 98 ('host', '8000', '/')
99 99 """
100 100 u = util.url(baseurl)
101 101 name = u.host or ''
102 102 if u.port:
103 103 port = u.port
104 104 path = u.path or ""
105 105 if not path.startswith('/'):
106 106 path = '/' + path
107 107
108 108 return name, str(port), path
109 109
110 110 class hgwebdir(object):
111 111 """HTTP server for multiple repositories.
112 112
113 113 Given a configuration, different repositories will be served depending
114 114 on the request path.
115 115
116 116 Instances are typically used as WSGI applications.
117 117 """
118 118 def __init__(self, conf, baseui=None):
119 119 self.conf = conf
120 120 self.baseui = baseui
121 121 self.ui = None
122 122 self.lastrefresh = 0
123 123 self.motd = None
124 124 self.refresh()
125 125
126 126 def refresh(self):
127 127 refreshinterval = 20
128 128 if self.ui:
129 129 refreshinterval = self.ui.configint('web', 'refreshinterval',
130 130 refreshinterval)
131 131
132 132 # refreshinterval <= 0 means to always refresh.
133 133 if (refreshinterval > 0 and
134 134 self.lastrefresh + refreshinterval > time.time()):
135 135 return
136 136
137 137 if self.baseui:
138 138 u = self.baseui.copy()
139 139 else:
140 140 u = uimod.ui.load()
141 141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
142 142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
143 143 # displaying bundling progress bar while serving feels wrong and may
144 144 # break some wsgi implementations.
145 145 u.setconfig('progress', 'disable', 'true', 'hgweb')
146 146
147 147 if not isinstance(self.conf, (dict, list, tuple)):
148 148 map = {'paths': 'hgweb-paths'}
149 149 if not os.path.exists(self.conf):
150 150 raise error.Abort(_('config file %s not found!') % self.conf)
151 151 u.readconfig(self.conf, remap=map, trust=True)
152 152 paths = []
153 153 for name, ignored in u.configitems('hgweb-paths'):
154 154 for path in u.configlist('hgweb-paths', name):
155 155 paths.append((name, path))
156 156 elif isinstance(self.conf, (list, tuple)):
157 157 paths = self.conf
158 158 elif isinstance(self.conf, dict):
159 159 paths = self.conf.items()
160 160
161 161 repos = findrepos(paths)
162 162 for prefix, root in u.configitems('collections'):
163 163 prefix = util.pconvert(prefix)
164 164 for path in scmutil.walkrepos(root, followsym=True):
165 165 repo = os.path.normpath(path)
166 166 name = util.pconvert(repo)
167 167 if name.startswith(prefix):
168 168 name = name[len(prefix):]
169 169 repos.append((name.lstrip('/'), repo))
170 170
171 171 self.repos = repos
172 172 self.ui = u
173 173 encoding.encoding = self.ui.config('web', 'encoding',
174 174 encoding.encoding)
175 175 self.style = self.ui.config('web', 'style', 'paper')
176 176 self.templatepath = self.ui.config('web', 'templates', None)
177 177 self.stripecount = self.ui.config('web', 'stripes', 1)
178 178 if self.stripecount:
179 179 self.stripecount = int(self.stripecount)
180 180 self._baseurl = self.ui.config('web', 'baseurl')
181 181 prefix = self.ui.config('web', 'prefix', '')
182 182 if prefix.startswith('/'):
183 183 prefix = prefix[1:]
184 184 if prefix.endswith('/'):
185 185 prefix = prefix[:-1]
186 186 self.prefix = prefix
187 187 self.lastrefresh = time.time()
188 188
189 189 def run(self):
190 190 if not encoding.environ.get('GATEWAY_INTERFACE',
191 191 '').startswith("CGI/1."):
192 192 raise RuntimeError("This function is only intended to be "
193 193 "called while running as a CGI script.")
194 194 wsgicgi.launch(self)
195 195
196 196 def __call__(self, env, respond):
197 197 req = wsgirequest(env, respond)
198 198 return self.run_wsgi(req)
199 199
200 200 def read_allowed(self, ui, req):
201 201 """Check allow_read and deny_read config options of a repo's ui object
202 202 to determine user permissions. By default, with neither option set (or
203 203 both empty), allow all users to read the repo. There are two ways a
204 204 user can be denied read access: (1) deny_read is not empty, and the
205 205 user is unauthenticated or deny_read contains user (or *), and (2)
206 206 allow_read is not empty and the user is not in allow_read. Return True
207 207 if user is allowed to read the repo, else return False."""
208 208
209 209 user = req.env.get('REMOTE_USER')
210 210
211 211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
212 212 if deny_read and (not user or ismember(ui, user, deny_read)):
213 213 return False
214 214
215 215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
216 216 # by default, allow reading if no allow_read option has been set
217 217 if (not allow_read) or ismember(ui, user, allow_read):
218 218 return True
219 219
220 220 return False
221 221
222 222 def run_wsgi(self, req):
223 223 profile = self.ui.configbool('profiling', 'enabled')
224 224 with profiling.profile(self.ui, enabled=profile):
225 225 for r in self._runwsgi(req):
226 226 yield r
227 227
228 228 def _runwsgi(self, req):
229 229 try:
230 230 self.refresh()
231 231
232 232 csp, nonce = cspvalues(self.ui)
233 233 if csp:
234 234 req.headers.append(('Content-Security-Policy', csp))
235 235
236 236 virtual = req.env.get("PATH_INFO", "").strip('/')
237 237 tmpl = self.templater(req, nonce)
238 238 ctype = tmpl('mimetype', encoding=encoding.encoding)
239 239 ctype = templater.stringify(ctype)
240 240
241 241 # a static file
242 242 if virtual.startswith('static/') or 'static' in req.form:
243 243 if virtual.startswith('static/'):
244 244 fname = virtual[7:]
245 245 else:
246 246 fname = req.form['static'][0]
247 247 static = self.ui.config("web", "static", None,
248 248 untrusted=False)
249 249 if not static:
250 250 tp = self.templatepath or templater.templatepaths()
251 251 if isinstance(tp, str):
252 252 tp = [tp]
253 253 static = [os.path.join(p, 'static') for p in tp]
254 254 staticfile(static, fname, req)
255 255 return []
256 256
257 257 # top-level index
258 258
259 259 repos = dict(self.repos)
260 260
261 261 if (not virtual or virtual == 'index') and virtual not in repos:
262 262 req.respond(HTTP_OK, ctype)
263 263 return self.makeindex(req, tmpl)
264 264
265 265 # nested indexes and hgwebs
266 266
267 267 if virtual.endswith('/index') and virtual not in repos:
268 268 subdir = virtual[:-len('index')]
269 269 if any(r.startswith(subdir) for r in repos):
270 270 req.respond(HTTP_OK, ctype)
271 271 return self.makeindex(req, tmpl, subdir)
272 272
273 273 def _virtualdirs():
274 274 # Check the full virtual path, each parent, and the root ('')
275 275 if virtual != '':
276 276 yield virtual
277 277
278 278 for p in util.finddirs(virtual):
279 279 yield p
280 280
281 281 yield ''
282 282
283 283 for virtualrepo in _virtualdirs():
284 284 real = repos.get(virtualrepo)
285 285 if real:
286 286 req.env['REPO_NAME'] = virtualrepo
287 287 try:
288 288 # ensure caller gets private copy of ui
289 289 repo = hg.repository(self.ui.copy(), real)
290 290 return hgweb_mod.hgweb(repo).run_wsgi(req)
291 291 except IOError as inst:
292 292 msg = encoding.strtolocal(inst.strerror)
293 293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
294 294 except error.RepoError as inst:
295 295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
296 296
297 297 # browse subdirectories
298 298 subdir = virtual + '/'
299 299 if [r for r in repos if r.startswith(subdir)]:
300 300 req.respond(HTTP_OK, ctype)
301 301 return self.makeindex(req, tmpl, subdir)
302 302
303 303 # prefixes not found
304 304 req.respond(HTTP_NOT_FOUND, ctype)
305 305 return tmpl("notfound", repo=virtual)
306 306
307 307 except ErrorResponse as err:
308 308 req.respond(err, ctype)
309 309 return tmpl('error', error=err.message or '')
310 310 finally:
311 311 tmpl = None
312 312
313 313 def makeindex(self, req, tmpl, subdir=""):
314 314
315 315 def archivelist(ui, nodeid, url):
316 316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
317 317 archives = []
318 318 for typ, spec in hgweb_mod.archivespecs.iteritems():
319 319 if typ in allowed or ui.configbool("web", "allow" + typ,
320 320 untrusted=True):
321 321 archives.append({"type" : typ, "extension": spec[2],
322 322 "node": nodeid, "url": url})
323 323 return archives
324 324
325 325 def rawentries(subdir="", **map):
326 326
327 327 descend = self.ui.configbool('web', 'descend')
328 328 collapse = self.ui.configbool('web', 'collapse')
329 329 seenrepos = set()
330 330 seendirs = set()
331 331 for name, path in self.repos:
332 332
333 333 if not name.startswith(subdir):
334 334 continue
335 335 name = name[len(subdir):]
336 336 directory = False
337 337
338 338 if '/' in name:
339 339 if not descend:
340 340 continue
341 341
342 342 nameparts = name.split('/')
343 343 rootname = nameparts[0]
344 344
345 345 if not collapse:
346 346 pass
347 347 elif rootname in seendirs:
348 348 continue
349 349 elif rootname in seenrepos:
350 350 pass
351 351 else:
352 352 directory = True
353 353 name = rootname
354 354
355 355 # redefine the path to refer to the directory
356 356 discarded = '/'.join(nameparts[1:])
357 357
358 358 # remove name parts plus accompanying slash
359 359 path = path[:-len(discarded) - 1]
360 360
361 361 try:
362 362 r = hg.repository(self.ui, path)
363 363 directory = False
364 364 except (IOError, error.RepoError):
365 365 pass
366 366
367 367 parts = [name]
368 368 parts.insert(0, '/' + subdir.rstrip('/'))
369 369 if req.env['SCRIPT_NAME']:
370 370 parts.insert(0, req.env['SCRIPT_NAME'])
371 371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
372 372
373 373 # show either a directory entry or a repository
374 374 if directory:
375 375 # get the directory's time information
376 376 try:
377 377 d = (get_mtime(path), util.makedate()[1])
378 378 except OSError:
379 379 continue
380 380
381 381 # add '/' to the name to make it obvious that
382 382 # the entry is a directory, not a regular repository
383 383 row = {'contact': "",
384 384 'contact_sort': "",
385 385 'name': name + '/',
386 386 'name_sort': name,
387 387 'url': url,
388 388 'description': "",
389 389 'description_sort': "",
390 390 'lastchange': d,
391 391 'lastchange_sort': d[1]-d[0],
392 392 'archives': [],
393 393 'isdirectory': True,
394 394 'labels': [],
395 395 }
396 396
397 397 seendirs.add(name)
398 398 yield row
399 399 continue
400 400
401 401 u = self.ui.copy()
402 402 try:
403 403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
404 404 except Exception as e:
405 405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
406 406 continue
407 407 def get(section, name, default=uimod._unset):
408 408 return u.config(section, name, default, untrusted=True)
409 409
410 410 if u.configbool("web", "hidden", untrusted=True):
411 411 continue
412 412
413 413 if not self.read_allowed(u, req):
414 414 continue
415 415
416 416 # update time with local timezone
417 417 try:
418 418 r = hg.repository(self.ui, path)
419 419 except IOError:
420 420 u.warn(_('error accessing repository at %s\n') % path)
421 421 continue
422 422 except error.RepoError:
423 423 u.warn(_('error accessing repository at %s\n') % path)
424 424 continue
425 425 try:
426 426 d = (get_mtime(r.spath), util.makedate()[1])
427 427 except OSError:
428 428 continue
429 429
430 430 contact = get_contact(get)
431 description = get("web", "description", "")
431 description = get("web", "description")
432 432 seenrepos.add(name)
433 433 name = get("web", "name", name)
434 434 row = {'contact': contact or "unknown",
435 435 'contact_sort': contact.upper() or "unknown",
436 436 'name': name,
437 437 'name_sort': name,
438 438 'url': url,
439 439 'description': description or "unknown",
440 440 'description_sort': description.upper() or "unknown",
441 441 'lastchange': d,
442 442 'lastchange_sort': d[1]-d[0],
443 443 'archives': archivelist(u, "tip", url),
444 444 'isdirectory': None,
445 445 'labels': u.configlist('web', 'labels', untrusted=True),
446 446 }
447 447
448 448 yield row
449 449
450 450 sortdefault = None, False
451 451 def entries(sortcolumn="", descending=False, subdir="", **map):
452 452 rows = rawentries(subdir=subdir, **map)
453 453
454 454 if sortcolumn and sortdefault != (sortcolumn, descending):
455 455 sortkey = '%s_sort' % sortcolumn
456 456 rows = sorted(rows, key=lambda x: x[sortkey],
457 457 reverse=descending)
458 458 for row, parity in zip(rows, paritygen(self.stripecount)):
459 459 row['parity'] = parity
460 460 yield row
461 461
462 462 self.refresh()
463 463 sortable = ["name", "description", "contact", "lastchange"]
464 464 sortcolumn, descending = sortdefault
465 465 if 'sort' in req.form:
466 466 sortcolumn = req.form['sort'][0]
467 467 descending = sortcolumn.startswith('-')
468 468 if descending:
469 469 sortcolumn = sortcolumn[1:]
470 470 if sortcolumn not in sortable:
471 471 sortcolumn = ""
472 472
473 473 sort = [("sort_%s" % column,
474 474 "%s%s" % ((not descending and column == sortcolumn)
475 475 and "-" or "", column))
476 476 for column in sortable]
477 477
478 478 self.refresh()
479 479 self.updatereqenv(req.env)
480 480
481 481 return tmpl("index", entries=entries, subdir=subdir,
482 482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
483 483 sortcolumn=sortcolumn, descending=descending,
484 484 **dict(sort))
485 485
486 486 def templater(self, req, nonce):
487 487
488 488 def motd(**map):
489 489 if self.motd is not None:
490 490 yield self.motd
491 491 else:
492 492 yield config('web', 'motd', '')
493 493
494 494 def config(section, name, default=uimod._unset, untrusted=True):
495 495 return self.ui.config(section, name, default, untrusted)
496 496
497 497 self.updatereqenv(req.env)
498 498
499 499 url = req.env.get('SCRIPT_NAME', '')
500 500 if not url.endswith('/'):
501 501 url += '/'
502 502
503 503 vars = {}
504 504 styles = (
505 505 req.form.get('style', [None])[0],
506 506 config('web', 'style'),
507 507 'paper'
508 508 )
509 509 style, mapfile = templater.stylemap(styles, self.templatepath)
510 510 if style == styles[0]:
511 511 vars['style'] = style
512 512
513 513 start = url[-1] == '?' and '&' or '?'
514 514 sessionvars = webutil.sessionvars(vars, start)
515 515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
516 516 logoimg = config('web', 'logoimg', 'hglogo.png')
517 517 staticurl = config('web', 'staticurl') or url + 'static/'
518 518 if not staticurl.endswith('/'):
519 519 staticurl += '/'
520 520
521 521 defaults = {
522 522 "encoding": encoding.encoding,
523 523 "motd": motd,
524 524 "url": url,
525 525 "logourl": logourl,
526 526 "logoimg": logoimg,
527 527 "staticurl": staticurl,
528 528 "sessionvars": sessionvars,
529 529 "style": style,
530 530 "nonce": nonce,
531 531 }
532 532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
533 533 return tmpl
534 534
535 535 def updatereqenv(self, env):
536 536 if self._baseurl is not None:
537 537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
538 538 env['SERVER_NAME'] = name
539 539 env['SERVER_PORT'] = port
540 540 env['SCRIPT_NAME'] = path
@@ -1,1385 +1,1388 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 11 import copy
12 12 import mimetypes
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_FORBIDDEN,
22 22 HTTP_NOT_FOUND,
23 23 HTTP_OK,
24 24 get_contact,
25 25 paritygen,
26 26 staticfile,
27 27 )
28 28
29 29 from .. import (
30 30 archival,
31 31 dagop,
32 32 encoding,
33 33 error,
34 34 graphmod,
35 35 revset,
36 36 revsetlang,
37 37 scmutil,
38 38 smartset,
39 39 templatefilters,
40 40 templater,
41 41 util,
42 42 )
43 43
44 44 from . import (
45 45 webutil,
46 46 )
47 47
48 48 __all__ = []
49 49 commands = {}
50 50
51 51 class webcommand(object):
52 52 """Decorator used to register a web command handler.
53 53
54 54 The decorator takes as its positional arguments the name/path the
55 55 command should be accessible under.
56 56
57 57 Usage:
58 58
59 59 @webcommand('mycommand')
60 60 def mycommand(web, req, tmpl):
61 61 pass
62 62 """
63 63
64 64 def __init__(self, name):
65 65 self.name = name
66 66
67 67 def __call__(self, func):
68 68 __all__.append(self.name)
69 69 commands[self.name] = func
70 70 return func
71 71
72 72 @webcommand('log')
73 73 def log(web, req, tmpl):
74 74 """
75 75 /log[/{revision}[/{path}]]
76 76 --------------------------
77 77
78 78 Show repository or file history.
79 79
80 80 For URLs of the form ``/log/{revision}``, a list of changesets starting at
81 81 the specified changeset identifier is shown. If ``{revision}`` is not
82 82 defined, the default is ``tip``. This form is equivalent to the
83 83 ``changelog`` handler.
84 84
85 85 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
86 86 file will be shown. This form is equivalent to the ``filelog`` handler.
87 87 """
88 88
89 89 if 'file' in req.form and req.form['file'][0]:
90 90 return filelog(web, req, tmpl)
91 91 else:
92 92 return changelog(web, req, tmpl)
93 93
94 94 @webcommand('rawfile')
95 95 def rawfile(web, req, tmpl):
96 96 guessmime = web.configbool('web', 'guessmime', False)
97 97
98 98 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
99 99 if not path:
100 100 content = manifest(web, req, tmpl)
101 101 req.respond(HTTP_OK, web.ctype)
102 102 return content
103 103
104 104 try:
105 105 fctx = webutil.filectx(web.repo, req)
106 106 except error.LookupError as inst:
107 107 try:
108 108 content = manifest(web, req, tmpl)
109 109 req.respond(HTTP_OK, web.ctype)
110 110 return content
111 111 except ErrorResponse:
112 112 raise inst
113 113
114 114 path = fctx.path()
115 115 text = fctx.data()
116 116 mt = 'application/binary'
117 117 if guessmime:
118 118 mt = mimetypes.guess_type(path)[0]
119 119 if mt is None:
120 120 if util.binary(text):
121 121 mt = 'application/binary'
122 122 else:
123 123 mt = 'text/plain'
124 124 if mt.startswith('text/'):
125 125 mt += '; charset="%s"' % encoding.encoding
126 126
127 127 req.respond(HTTP_OK, mt, path, body=text)
128 128 return []
129 129
130 130 def _filerevision(web, req, tmpl, fctx):
131 131 f = fctx.path()
132 132 text = fctx.data()
133 133 parity = paritygen(web.stripecount)
134 134 ishead = fctx.filerev() in fctx.filelog().headrevs()
135 135
136 136 if util.binary(text):
137 137 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
138 138 text = '(binary:%s)' % mt
139 139
140 140 def lines():
141 141 for lineno, t in enumerate(text.splitlines(True)):
142 142 yield {"line": t,
143 143 "lineid": "l%d" % (lineno + 1),
144 144 "linenumber": "% 6d" % (lineno + 1),
145 145 "parity": next(parity)}
146 146
147 147 return tmpl("filerevision",
148 148 file=f,
149 149 path=webutil.up(f),
150 150 text=lines(),
151 151 symrev=webutil.symrevorshortnode(req, fctx),
152 152 rename=webutil.renamelink(fctx),
153 153 permissions=fctx.manifest().flags(f),
154 154 ishead=int(ishead),
155 155 **webutil.commonentry(web.repo, fctx))
156 156
157 157 @webcommand('file')
158 158 def file(web, req, tmpl):
159 159 """
160 160 /file/{revision}[/{path}]
161 161 -------------------------
162 162
163 163 Show information about a directory or file in the repository.
164 164
165 165 Info about the ``path`` given as a URL parameter will be rendered.
166 166
167 167 If ``path`` is a directory, information about the entries in that
168 168 directory will be rendered. This form is equivalent to the ``manifest``
169 169 handler.
170 170
171 171 If ``path`` is a file, information about that file will be shown via
172 172 the ``filerevision`` template.
173 173
174 174 If ``path`` is not defined, information about the root directory will
175 175 be rendered.
176 176 """
177 177 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
178 178 if not path:
179 179 return manifest(web, req, tmpl)
180 180 try:
181 181 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
182 182 except error.LookupError as inst:
183 183 try:
184 184 return manifest(web, req, tmpl)
185 185 except ErrorResponse:
186 186 raise inst
187 187
188 188 def _search(web, req, tmpl):
189 189 MODE_REVISION = 'rev'
190 190 MODE_KEYWORD = 'keyword'
191 191 MODE_REVSET = 'revset'
192 192
193 193 def revsearch(ctx):
194 194 yield ctx
195 195
196 196 def keywordsearch(query):
197 197 lower = encoding.lower
198 198 qw = lower(query).split()
199 199
200 200 def revgen():
201 201 cl = web.repo.changelog
202 202 for i in xrange(len(web.repo) - 1, 0, -100):
203 203 l = []
204 204 for j in cl.revs(max(0, i - 99), i):
205 205 ctx = web.repo[j]
206 206 l.append(ctx)
207 207 l.reverse()
208 208 for e in l:
209 209 yield e
210 210
211 211 for ctx in revgen():
212 212 miss = 0
213 213 for q in qw:
214 214 if not (q in lower(ctx.user()) or
215 215 q in lower(ctx.description()) or
216 216 q in lower(" ".join(ctx.files()))):
217 217 miss = 1
218 218 break
219 219 if miss:
220 220 continue
221 221
222 222 yield ctx
223 223
224 224 def revsetsearch(revs):
225 225 for r in revs:
226 226 yield web.repo[r]
227 227
228 228 searchfuncs = {
229 229 MODE_REVISION: (revsearch, 'exact revision search'),
230 230 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
231 231 MODE_REVSET: (revsetsearch, 'revset expression search'),
232 232 }
233 233
234 234 def getsearchmode(query):
235 235 try:
236 236 ctx = web.repo[query]
237 237 except (error.RepoError, error.LookupError):
238 238 # query is not an exact revision pointer, need to
239 239 # decide if it's a revset expression or keywords
240 240 pass
241 241 else:
242 242 return MODE_REVISION, ctx
243 243
244 244 revdef = 'reverse(%s)' % query
245 245 try:
246 246 tree = revsetlang.parse(revdef)
247 247 except error.ParseError:
248 248 # can't parse to a revset tree
249 249 return MODE_KEYWORD, query
250 250
251 251 if revsetlang.depth(tree) <= 2:
252 252 # no revset syntax used
253 253 return MODE_KEYWORD, query
254 254
255 255 if any((token, (value or '')[:3]) == ('string', 're:')
256 256 for token, value, pos in revsetlang.tokenize(revdef)):
257 257 return MODE_KEYWORD, query
258 258
259 259 funcsused = revsetlang.funcsused(tree)
260 260 if not funcsused.issubset(revset.safesymbols):
261 261 return MODE_KEYWORD, query
262 262
263 263 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
264 264 try:
265 265 revs = mfunc(web.repo)
266 266 return MODE_REVSET, revs
267 267 # ParseError: wrongly placed tokens, wrongs arguments, etc
268 268 # RepoLookupError: no such revision, e.g. in 'revision:'
269 269 # Abort: bookmark/tag not exists
270 270 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
271 271 except (error.ParseError, error.RepoLookupError, error.Abort,
272 272 LookupError):
273 273 return MODE_KEYWORD, query
274 274
275 275 def changelist(**map):
276 276 count = 0
277 277
278 278 for ctx in searchfunc[0](funcarg):
279 279 count += 1
280 280 n = ctx.node()
281 281 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
282 282 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
283 283
284 284 yield tmpl('searchentry',
285 285 parity=next(parity),
286 286 changelogtag=showtags,
287 287 files=files,
288 288 **webutil.commonentry(web.repo, ctx))
289 289
290 290 if count >= revcount:
291 291 break
292 292
293 293 query = req.form['rev'][0]
294 294 revcount = web.maxchanges
295 295 if 'revcount' in req.form:
296 296 try:
297 297 revcount = int(req.form.get('revcount', [revcount])[0])
298 298 revcount = max(revcount, 1)
299 299 tmpl.defaults['sessionvars']['revcount'] = revcount
300 300 except ValueError:
301 301 pass
302 302
303 303 lessvars = copy.copy(tmpl.defaults['sessionvars'])
304 304 lessvars['revcount'] = max(revcount / 2, 1)
305 305 lessvars['rev'] = query
306 306 morevars = copy.copy(tmpl.defaults['sessionvars'])
307 307 morevars['revcount'] = revcount * 2
308 308 morevars['rev'] = query
309 309
310 310 mode, funcarg = getsearchmode(query)
311 311
312 312 if 'forcekw' in req.form:
313 313 showforcekw = ''
314 314 showunforcekw = searchfuncs[mode][1]
315 315 mode = MODE_KEYWORD
316 316 funcarg = query
317 317 else:
318 318 if mode != MODE_KEYWORD:
319 319 showforcekw = searchfuncs[MODE_KEYWORD][1]
320 320 else:
321 321 showforcekw = ''
322 322 showunforcekw = ''
323 323
324 324 searchfunc = searchfuncs[mode]
325 325
326 326 tip = web.repo['tip']
327 327 parity = paritygen(web.stripecount)
328 328
329 329 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
330 330 entries=changelist, archives=web.archivelist("tip"),
331 331 morevars=morevars, lessvars=lessvars,
332 332 modedesc=searchfunc[1],
333 333 showforcekw=showforcekw, showunforcekw=showunforcekw)
334 334
335 335 @webcommand('changelog')
336 336 def changelog(web, req, tmpl, shortlog=False):
337 337 """
338 338 /changelog[/{revision}]
339 339 -----------------------
340 340
341 341 Show information about multiple changesets.
342 342
343 343 If the optional ``revision`` URL argument is absent, information about
344 344 all changesets starting at ``tip`` will be rendered. If the ``revision``
345 345 argument is present, changesets will be shown starting from the specified
346 346 revision.
347 347
348 348 If ``revision`` is absent, the ``rev`` query string argument may be
349 349 defined. This will perform a search for changesets.
350 350
351 351 The argument for ``rev`` can be a single revision, a revision set,
352 352 or a literal keyword to search for in changeset data (equivalent to
353 353 :hg:`log -k`).
354 354
355 355 The ``revcount`` query string argument defines the maximum numbers of
356 356 changesets to render.
357 357
358 358 For non-searches, the ``changelog`` template will be rendered.
359 359 """
360 360
361 361 query = ''
362 362 if 'node' in req.form:
363 363 ctx = webutil.changectx(web.repo, req)
364 364 symrev = webutil.symrevorshortnode(req, ctx)
365 365 elif 'rev' in req.form:
366 366 return _search(web, req, tmpl)
367 367 else:
368 368 ctx = web.repo['tip']
369 369 symrev = 'tip'
370 370
371 371 def changelist():
372 372 revs = []
373 373 if pos != -1:
374 374 revs = web.repo.changelog.revs(pos, 0)
375 375 curcount = 0
376 376 for rev in revs:
377 377 curcount += 1
378 378 if curcount > revcount + 1:
379 379 break
380 380
381 381 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
382 382 entry['parity'] = next(parity)
383 383 yield entry
384 384
385 385 if shortlog:
386 386 revcount = web.maxshortchanges
387 387 else:
388 388 revcount = web.maxchanges
389 389
390 390 if 'revcount' in req.form:
391 391 try:
392 392 revcount = int(req.form.get('revcount', [revcount])[0])
393 393 revcount = max(revcount, 1)
394 394 tmpl.defaults['sessionvars']['revcount'] = revcount
395 395 except ValueError:
396 396 pass
397 397
398 398 lessvars = copy.copy(tmpl.defaults['sessionvars'])
399 399 lessvars['revcount'] = max(revcount / 2, 1)
400 400 morevars = copy.copy(tmpl.defaults['sessionvars'])
401 401 morevars['revcount'] = revcount * 2
402 402
403 403 count = len(web.repo)
404 404 pos = ctx.rev()
405 405 parity = paritygen(web.stripecount)
406 406
407 407 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
408 408
409 409 entries = list(changelist())
410 410 latestentry = entries[:1]
411 411 if len(entries) > revcount:
412 412 nextentry = entries[-1:]
413 413 entries = entries[:-1]
414 414 else:
415 415 nextentry = []
416 416
417 417 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
418 418 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
419 419 entries=entries,
420 420 latestentry=latestentry, nextentry=nextentry,
421 421 archives=web.archivelist("tip"), revcount=revcount,
422 422 morevars=morevars, lessvars=lessvars, query=query)
423 423
424 424 @webcommand('shortlog')
425 425 def shortlog(web, req, tmpl):
426 426 """
427 427 /shortlog
428 428 ---------
429 429
430 430 Show basic information about a set of changesets.
431 431
432 432 This accepts the same parameters as the ``changelog`` handler. The only
433 433 difference is the ``shortlog`` template will be rendered instead of the
434 434 ``changelog`` template.
435 435 """
436 436 return changelog(web, req, tmpl, shortlog=True)
437 437
438 438 @webcommand('changeset')
439 439 def changeset(web, req, tmpl):
440 440 """
441 441 /changeset[/{revision}]
442 442 -----------------------
443 443
444 444 Show information about a single changeset.
445 445
446 446 A URL path argument is the changeset identifier to show. See ``hg help
447 447 revisions`` for possible values. If not defined, the ``tip`` changeset
448 448 will be shown.
449 449
450 450 The ``changeset`` template is rendered. Contents of the ``changesettag``,
451 451 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
452 452 templates related to diffs may all be used to produce the output.
453 453 """
454 454 ctx = webutil.changectx(web.repo, req)
455 455
456 456 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
457 457
458 458 rev = webcommand('rev')(changeset)
459 459
460 460 def decodepath(path):
461 461 """Hook for mapping a path in the repository to a path in the
462 462 working copy.
463 463
464 464 Extensions (e.g., largefiles) can override this to remap files in
465 465 the virtual file system presented by the manifest command below."""
466 466 return path
467 467
468 468 @webcommand('manifest')
469 469 def manifest(web, req, tmpl):
470 470 """
471 471 /manifest[/{revision}[/{path}]]
472 472 -------------------------------
473 473
474 474 Show information about a directory.
475 475
476 476 If the URL path arguments are omitted, information about the root
477 477 directory for the ``tip`` changeset will be shown.
478 478
479 479 Because this handler can only show information for directories, it
480 480 is recommended to use the ``file`` handler instead, as it can handle both
481 481 directories and files.
482 482
483 483 The ``manifest`` template will be rendered for this handler.
484 484 """
485 485 if 'node' in req.form:
486 486 ctx = webutil.changectx(web.repo, req)
487 487 symrev = webutil.symrevorshortnode(req, ctx)
488 488 else:
489 489 ctx = web.repo['tip']
490 490 symrev = 'tip'
491 491 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
492 492 mf = ctx.manifest()
493 493 node = ctx.node()
494 494
495 495 files = {}
496 496 dirs = {}
497 497 parity = paritygen(web.stripecount)
498 498
499 499 if path and path[-1] != "/":
500 500 path += "/"
501 501 l = len(path)
502 502 abspath = "/" + path
503 503
504 504 for full, n in mf.iteritems():
505 505 # the virtual path (working copy path) used for the full
506 506 # (repository) path
507 507 f = decodepath(full)
508 508
509 509 if f[:l] != path:
510 510 continue
511 511 remain = f[l:]
512 512 elements = remain.split('/')
513 513 if len(elements) == 1:
514 514 files[remain] = full
515 515 else:
516 516 h = dirs # need to retain ref to dirs (root)
517 517 for elem in elements[0:-1]:
518 518 if elem not in h:
519 519 h[elem] = {}
520 520 h = h[elem]
521 521 if len(h) > 1:
522 522 break
523 523 h[None] = None # denotes files present
524 524
525 525 if mf and not files and not dirs:
526 526 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
527 527
528 528 def filelist(**map):
529 529 for f in sorted(files):
530 530 full = files[f]
531 531
532 532 fctx = ctx.filectx(full)
533 533 yield {"file": full,
534 534 "parity": next(parity),
535 535 "basename": f,
536 536 "date": fctx.date(),
537 537 "size": fctx.size(),
538 538 "permissions": mf.flags(full)}
539 539
540 540 def dirlist(**map):
541 541 for d in sorted(dirs):
542 542
543 543 emptydirs = []
544 544 h = dirs[d]
545 545 while isinstance(h, dict) and len(h) == 1:
546 546 k, v = h.items()[0]
547 547 if v:
548 548 emptydirs.append(k)
549 549 h = v
550 550
551 551 path = "%s%s" % (abspath, d)
552 552 yield {"parity": next(parity),
553 553 "path": path,
554 554 "emptydirs": "/".join(emptydirs),
555 555 "basename": d}
556 556
557 557 return tmpl("manifest",
558 558 symrev=symrev,
559 559 path=abspath,
560 560 up=webutil.up(abspath),
561 561 upparity=next(parity),
562 562 fentries=filelist,
563 563 dentries=dirlist,
564 564 archives=web.archivelist(hex(node)),
565 565 **webutil.commonentry(web.repo, ctx))
566 566
567 567 @webcommand('tags')
568 568 def tags(web, req, tmpl):
569 569 """
570 570 /tags
571 571 -----
572 572
573 573 Show information about tags.
574 574
575 575 No arguments are accepted.
576 576
577 577 The ``tags`` template is rendered.
578 578 """
579 579 i = list(reversed(web.repo.tagslist()))
580 580 parity = paritygen(web.stripecount)
581 581
582 582 def entries(notip, latestonly, **map):
583 583 t = i
584 584 if notip:
585 585 t = [(k, n) for k, n in i if k != "tip"]
586 586 if latestonly:
587 587 t = t[:1]
588 588 for k, n in t:
589 589 yield {"parity": next(parity),
590 590 "tag": k,
591 591 "date": web.repo[n].date(),
592 592 "node": hex(n)}
593 593
594 594 return tmpl("tags",
595 595 node=hex(web.repo.changelog.tip()),
596 596 entries=lambda **x: entries(False, False, **x),
597 597 entriesnotip=lambda **x: entries(True, False, **x),
598 598 latestentry=lambda **x: entries(True, True, **x))
599 599
600 600 @webcommand('bookmarks')
601 601 def bookmarks(web, req, tmpl):
602 602 """
603 603 /bookmarks
604 604 ----------
605 605
606 606 Show information about bookmarks.
607 607
608 608 No arguments are accepted.
609 609
610 610 The ``bookmarks`` template is rendered.
611 611 """
612 612 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
613 613 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
614 614 i = sorted(i, key=sortkey, reverse=True)
615 615 parity = paritygen(web.stripecount)
616 616
617 617 def entries(latestonly, **map):
618 618 t = i
619 619 if latestonly:
620 620 t = i[:1]
621 621 for k, n in t:
622 622 yield {"parity": next(parity),
623 623 "bookmark": k,
624 624 "date": web.repo[n].date(),
625 625 "node": hex(n)}
626 626
627 627 if i:
628 628 latestrev = i[0][1]
629 629 else:
630 630 latestrev = -1
631 631
632 632 return tmpl("bookmarks",
633 633 node=hex(web.repo.changelog.tip()),
634 634 lastchange=[{"date": web.repo[latestrev].date()}],
635 635 entries=lambda **x: entries(latestonly=False, **x),
636 636 latestentry=lambda **x: entries(latestonly=True, **x))
637 637
638 638 @webcommand('branches')
639 639 def branches(web, req, tmpl):
640 640 """
641 641 /branches
642 642 ---------
643 643
644 644 Show information about branches.
645 645
646 646 All known branches are contained in the output, even closed branches.
647 647
648 648 No arguments are accepted.
649 649
650 650 The ``branches`` template is rendered.
651 651 """
652 652 entries = webutil.branchentries(web.repo, web.stripecount)
653 653 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
654 654 return tmpl('branches', node=hex(web.repo.changelog.tip()),
655 655 entries=entries, latestentry=latestentry)
656 656
657 657 @webcommand('summary')
658 658 def summary(web, req, tmpl):
659 659 """
660 660 /summary
661 661 --------
662 662
663 663 Show a summary of repository state.
664 664
665 665 Information about the latest changesets, bookmarks, tags, and branches
666 666 is captured by this handler.
667 667
668 668 The ``summary`` template is rendered.
669 669 """
670 670 i = reversed(web.repo.tagslist())
671 671
672 672 def tagentries(**map):
673 673 parity = paritygen(web.stripecount)
674 674 count = 0
675 675 for k, n in i:
676 676 if k == "tip": # skip tip
677 677 continue
678 678
679 679 count += 1
680 680 if count > 10: # limit to 10 tags
681 681 break
682 682
683 683 yield tmpl("tagentry",
684 684 parity=next(parity),
685 685 tag=k,
686 686 node=hex(n),
687 687 date=web.repo[n].date())
688 688
689 689 def bookmarks(**map):
690 690 parity = paritygen(web.stripecount)
691 691 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
692 692 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
693 693 marks = sorted(marks, key=sortkey, reverse=True)
694 694 for k, n in marks[:10]: # limit to 10 bookmarks
695 695 yield {'parity': next(parity),
696 696 'bookmark': k,
697 697 'date': web.repo[n].date(),
698 698 'node': hex(n)}
699 699
700 700 def changelist(**map):
701 701 parity = paritygen(web.stripecount, offset=start - end)
702 702 l = [] # build a list in forward order for efficiency
703 703 revs = []
704 704 if start < end:
705 705 revs = web.repo.changelog.revs(start, end - 1)
706 706 for i in revs:
707 707 ctx = web.repo[i]
708 708
709 709 l.append(tmpl(
710 710 'shortlogentry',
711 711 parity=next(parity),
712 712 **webutil.commonentry(web.repo, ctx)))
713 713
714 714 for entry in reversed(l):
715 715 yield entry
716 716
717 717 tip = web.repo['tip']
718 718 count = len(web.repo)
719 719 start = max(0, count - web.maxchanges)
720 720 end = min(count, start + web.maxchanges)
721 721
722 desc = web.config("web", "description")
723 if not desc:
724 desc = 'unknown'
722 725 return tmpl("summary",
723 desc=web.config("web", "description", "unknown"),
726 desc=desc,
724 727 owner=get_contact(web.config) or "unknown",
725 728 lastchange=tip.date(),
726 729 tags=tagentries,
727 730 bookmarks=bookmarks,
728 731 branches=webutil.branchentries(web.repo, web.stripecount, 10),
729 732 shortlog=changelist,
730 733 node=tip.hex(),
731 734 symrev='tip',
732 735 archives=web.archivelist("tip"),
733 736 labels=web.configlist('web', 'labels'))
734 737
735 738 @webcommand('filediff')
736 739 def filediff(web, req, tmpl):
737 740 """
738 741 /diff/{revision}/{path}
739 742 -----------------------
740 743
741 744 Show how a file changed in a particular commit.
742 745
743 746 The ``filediff`` template is rendered.
744 747
745 748 This handler is registered under both the ``/diff`` and ``/filediff``
746 749 paths. ``/diff`` is used in modern code.
747 750 """
748 751 fctx, ctx = None, None
749 752 try:
750 753 fctx = webutil.filectx(web.repo, req)
751 754 except LookupError:
752 755 ctx = webutil.changectx(web.repo, req)
753 756 path = webutil.cleanpath(web.repo, req.form['file'][0])
754 757 if path not in ctx.files():
755 758 raise
756 759
757 760 if fctx is not None:
758 761 path = fctx.path()
759 762 ctx = fctx.changectx()
760 763 basectx = ctx.p1()
761 764
762 765 style = web.config('web', 'style', 'paper')
763 766 if 'style' in req.form:
764 767 style = req.form['style'][0]
765 768
766 769 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
767 770 if fctx is not None:
768 771 rename = webutil.renamelink(fctx)
769 772 ctx = fctx
770 773 else:
771 774 rename = []
772 775 ctx = ctx
773 776 return tmpl("filediff",
774 777 file=path,
775 778 symrev=webutil.symrevorshortnode(req, ctx),
776 779 rename=rename,
777 780 diff=diffs,
778 781 **webutil.commonentry(web.repo, ctx))
779 782
780 783 diff = webcommand('diff')(filediff)
781 784
782 785 @webcommand('comparison')
783 786 def comparison(web, req, tmpl):
784 787 """
785 788 /comparison/{revision}/{path}
786 789 -----------------------------
787 790
788 791 Show a comparison between the old and new versions of a file from changes
789 792 made on a particular revision.
790 793
791 794 This is similar to the ``diff`` handler. However, this form features
792 795 a split or side-by-side diff rather than a unified diff.
793 796
794 797 The ``context`` query string argument can be used to control the lines of
795 798 context in the diff.
796 799
797 800 The ``filecomparison`` template is rendered.
798 801 """
799 802 ctx = webutil.changectx(web.repo, req)
800 803 if 'file' not in req.form:
801 804 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
802 805 path = webutil.cleanpath(web.repo, req.form['file'][0])
803 806
804 807 parsecontext = lambda v: v == 'full' and -1 or int(v)
805 808 if 'context' in req.form:
806 809 context = parsecontext(req.form['context'][0])
807 810 else:
808 811 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
809 812
810 813 def filelines(f):
811 814 if f.isbinary():
812 815 mt = mimetypes.guess_type(f.path())[0]
813 816 if not mt:
814 817 mt = 'application/octet-stream'
815 818 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
816 819 return f.data().splitlines()
817 820
818 821 fctx = None
819 822 parent = ctx.p1()
820 823 leftrev = parent.rev()
821 824 leftnode = parent.node()
822 825 rightrev = ctx.rev()
823 826 rightnode = ctx.node()
824 827 if path in ctx:
825 828 fctx = ctx[path]
826 829 rightlines = filelines(fctx)
827 830 if path not in parent:
828 831 leftlines = ()
829 832 else:
830 833 pfctx = parent[path]
831 834 leftlines = filelines(pfctx)
832 835 else:
833 836 rightlines = ()
834 837 pfctx = ctx.parents()[0][path]
835 838 leftlines = filelines(pfctx)
836 839
837 840 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
838 841 if fctx is not None:
839 842 rename = webutil.renamelink(fctx)
840 843 ctx = fctx
841 844 else:
842 845 rename = []
843 846 ctx = ctx
844 847 return tmpl('filecomparison',
845 848 file=path,
846 849 symrev=webutil.symrevorshortnode(req, ctx),
847 850 rename=rename,
848 851 leftrev=leftrev,
849 852 leftnode=hex(leftnode),
850 853 rightrev=rightrev,
851 854 rightnode=hex(rightnode),
852 855 comparison=comparison,
853 856 **webutil.commonentry(web.repo, ctx))
854 857
855 858 @webcommand('annotate')
856 859 def annotate(web, req, tmpl):
857 860 """
858 861 /annotate/{revision}/{path}
859 862 ---------------------------
860 863
861 864 Show changeset information for each line in a file.
862 865
863 866 The ``fileannotate`` template is rendered.
864 867 """
865 868 fctx = webutil.filectx(web.repo, req)
866 869 f = fctx.path()
867 870 parity = paritygen(web.stripecount)
868 871 ishead = fctx.filerev() in fctx.filelog().headrevs()
869 872
870 873 # parents() is called once per line and several lines likely belong to
871 874 # same revision. So it is worth caching.
872 875 # TODO there are still redundant operations within basefilectx.parents()
873 876 # and from the fctx.annotate() call itself that could be cached.
874 877 parentscache = {}
875 878 def parents(f):
876 879 rev = f.rev()
877 880 if rev not in parentscache:
878 881 parentscache[rev] = []
879 882 for p in f.parents():
880 883 entry = {
881 884 'node': p.hex(),
882 885 'rev': p.rev(),
883 886 }
884 887 parentscache[rev].append(entry)
885 888
886 889 for p in parentscache[rev]:
887 890 yield p
888 891
889 892 def annotate(**map):
890 893 if fctx.isbinary():
891 894 mt = (mimetypes.guess_type(fctx.path())[0]
892 895 or 'application/octet-stream')
893 896 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
894 897 else:
895 898 lines = webutil.annotate(fctx, web.repo.ui)
896 899
897 900 previousrev = None
898 901 blockparitygen = paritygen(1)
899 902 for lineno, ((f, targetline), l) in enumerate(lines):
900 903 rev = f.rev()
901 904 if rev != previousrev:
902 905 blockhead = True
903 906 blockparity = next(blockparitygen)
904 907 else:
905 908 blockhead = None
906 909 previousrev = rev
907 910 yield {"parity": next(parity),
908 911 "node": f.hex(),
909 912 "rev": rev,
910 913 "author": f.user(),
911 914 "parents": parents(f),
912 915 "desc": f.description(),
913 916 "extra": f.extra(),
914 917 "file": f.path(),
915 918 "blockhead": blockhead,
916 919 "blockparity": blockparity,
917 920 "targetline": targetline,
918 921 "line": l,
919 922 "lineno": lineno + 1,
920 923 "lineid": "l%d" % (lineno + 1),
921 924 "linenumber": "% 6d" % (lineno + 1),
922 925 "revdate": f.date()}
923 926
924 927 return tmpl("fileannotate",
925 928 file=f,
926 929 annotate=annotate,
927 930 path=webutil.up(f),
928 931 symrev=webutil.symrevorshortnode(req, fctx),
929 932 rename=webutil.renamelink(fctx),
930 933 permissions=fctx.manifest().flags(f),
931 934 ishead=int(ishead),
932 935 **webutil.commonentry(web.repo, fctx))
933 936
934 937 @webcommand('filelog')
935 938 def filelog(web, req, tmpl):
936 939 """
937 940 /filelog/{revision}/{path}
938 941 --------------------------
939 942
940 943 Show information about the history of a file in the repository.
941 944
942 945 The ``revcount`` query string argument can be defined to control the
943 946 maximum number of entries to show.
944 947
945 948 The ``filelog`` template will be rendered.
946 949 """
947 950
948 951 try:
949 952 fctx = webutil.filectx(web.repo, req)
950 953 f = fctx.path()
951 954 fl = fctx.filelog()
952 955 except error.LookupError:
953 956 f = webutil.cleanpath(web.repo, req.form['file'][0])
954 957 fl = web.repo.file(f)
955 958 numrevs = len(fl)
956 959 if not numrevs: # file doesn't exist at all
957 960 raise
958 961 rev = webutil.changectx(web.repo, req).rev()
959 962 first = fl.linkrev(0)
960 963 if rev < first: # current rev is from before file existed
961 964 raise
962 965 frev = numrevs - 1
963 966 while fl.linkrev(frev) > rev:
964 967 frev -= 1
965 968 fctx = web.repo.filectx(f, fl.linkrev(frev))
966 969
967 970 revcount = web.maxshortchanges
968 971 if 'revcount' in req.form:
969 972 try:
970 973 revcount = int(req.form.get('revcount', [revcount])[0])
971 974 revcount = max(revcount, 1)
972 975 tmpl.defaults['sessionvars']['revcount'] = revcount
973 976 except ValueError:
974 977 pass
975 978
976 979 lrange = webutil.linerange(req)
977 980
978 981 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 982 lessvars['revcount'] = max(revcount / 2, 1)
980 983 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 984 morevars['revcount'] = revcount * 2
982 985
983 986 patch = 'patch' in req.form
984 987 if patch:
985 988 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
986 989 descend = 'descend' in req.form
987 990 if descend:
988 991 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
989 992
990 993 count = fctx.filerev() + 1
991 994 start = max(0, count - revcount) # first rev on this page
992 995 end = min(count, start + revcount) # last rev on this page
993 996 parity = paritygen(web.stripecount, offset=start - end)
994 997
995 998 repo = web.repo
996 999 revs = fctx.filelog().revs(start, end - 1)
997 1000 entries = []
998 1001
999 1002 diffstyle = web.config('web', 'style', 'paper')
1000 1003 if 'style' in req.form:
1001 1004 diffstyle = req.form['style'][0]
1002 1005
1003 1006 def diff(fctx, linerange=None):
1004 1007 ctx = fctx.changectx()
1005 1008 basectx = ctx.p1()
1006 1009 path = fctx.path()
1007 1010 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1008 1011 linerange=linerange,
1009 1012 lineidprefix='%s-' % ctx.hex()[:12])
1010 1013
1011 1014 linerange = None
1012 1015 if lrange is not None:
1013 1016 linerange = webutil.formatlinerange(*lrange)
1014 1017 # deactivate numeric nav links when linerange is specified as this
1015 1018 # would required a dedicated "revnav" class
1016 1019 nav = None
1017 1020 if descend:
1018 1021 it = dagop.blockdescendants(fctx, *lrange)
1019 1022 else:
1020 1023 it = dagop.blockancestors(fctx, *lrange)
1021 1024 for i, (c, lr) in enumerate(it, 1):
1022 1025 diffs = None
1023 1026 if patch:
1024 1027 diffs = diff(c, linerange=lr)
1025 1028 # follow renames accross filtered (not in range) revisions
1026 1029 path = c.path()
1027 1030 entries.append(dict(
1028 1031 parity=next(parity),
1029 1032 filerev=c.rev(),
1030 1033 file=path,
1031 1034 diff=diffs,
1032 1035 linerange=webutil.formatlinerange(*lr),
1033 1036 **webutil.commonentry(repo, c)))
1034 1037 if i == revcount:
1035 1038 break
1036 1039 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1037 1040 morevars['linerange'] = lessvars['linerange']
1038 1041 else:
1039 1042 for i in revs:
1040 1043 iterfctx = fctx.filectx(i)
1041 1044 diffs = None
1042 1045 if patch:
1043 1046 diffs = diff(iterfctx)
1044 1047 entries.append(dict(
1045 1048 parity=next(parity),
1046 1049 filerev=i,
1047 1050 file=f,
1048 1051 diff=diffs,
1049 1052 rename=webutil.renamelink(iterfctx),
1050 1053 **webutil.commonentry(repo, iterfctx)))
1051 1054 entries.reverse()
1052 1055 revnav = webutil.filerevnav(web.repo, fctx.path())
1053 1056 nav = revnav.gen(end - 1, revcount, count)
1054 1057
1055 1058 latestentry = entries[:1]
1056 1059
1057 1060 return tmpl("filelog",
1058 1061 file=f,
1059 1062 nav=nav,
1060 1063 symrev=webutil.symrevorshortnode(req, fctx),
1061 1064 entries=entries,
1062 1065 descend=descend,
1063 1066 patch=patch,
1064 1067 latestentry=latestentry,
1065 1068 linerange=linerange,
1066 1069 revcount=revcount,
1067 1070 morevars=morevars,
1068 1071 lessvars=lessvars,
1069 1072 **webutil.commonentry(web.repo, fctx))
1070 1073
1071 1074 @webcommand('archive')
1072 1075 def archive(web, req, tmpl):
1073 1076 """
1074 1077 /archive/{revision}.{format}[/{path}]
1075 1078 -------------------------------------
1076 1079
1077 1080 Obtain an archive of repository content.
1078 1081
1079 1082 The content and type of the archive is defined by a URL path parameter.
1080 1083 ``format`` is the file extension of the archive type to be generated. e.g.
1081 1084 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1082 1085 server configuration.
1083 1086
1084 1087 The optional ``path`` URL parameter controls content to include in the
1085 1088 archive. If omitted, every file in the specified revision is present in the
1086 1089 archive. If included, only the specified file or contents of the specified
1087 1090 directory will be included in the archive.
1088 1091
1089 1092 No template is used for this handler. Raw, binary content is generated.
1090 1093 """
1091 1094
1092 1095 type_ = req.form.get('type', [None])[0]
1093 1096 allowed = web.configlist("web", "allow_archive")
1094 1097 key = req.form['node'][0]
1095 1098
1096 1099 if type_ not in web.archivespecs:
1097 1100 msg = 'Unsupported archive type: %s' % type_
1098 1101 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1099 1102
1100 1103 if not ((type_ in allowed or
1101 1104 web.configbool("web", "allow" + type_, False))):
1102 1105 msg = 'Archive type not allowed: %s' % type_
1103 1106 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1104 1107
1105 1108 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1106 1109 cnode = web.repo.lookup(key)
1107 1110 arch_version = key
1108 1111 if cnode == key or key == 'tip':
1109 1112 arch_version = short(cnode)
1110 1113 name = "%s-%s" % (reponame, arch_version)
1111 1114
1112 1115 ctx = webutil.changectx(web.repo, req)
1113 1116 pats = []
1114 1117 match = scmutil.match(ctx, [])
1115 1118 file = req.form.get('file', None)
1116 1119 if file:
1117 1120 pats = ['path:' + file[0]]
1118 1121 match = scmutil.match(ctx, pats, default='path')
1119 1122 if pats:
1120 1123 files = [f for f in ctx.manifest().keys() if match(f)]
1121 1124 if not files:
1122 1125 raise ErrorResponse(HTTP_NOT_FOUND,
1123 1126 'file(s) not found: %s' % file[0])
1124 1127
1125 1128 mimetype, artype, extension, encoding = web.archivespecs[type_]
1126 1129 headers = [
1127 1130 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1128 1131 ]
1129 1132 if encoding:
1130 1133 headers.append(('Content-Encoding', encoding))
1131 1134 req.headers.extend(headers)
1132 1135 req.respond(HTTP_OK, mimetype)
1133 1136
1134 1137 archival.archive(web.repo, req, cnode, artype, prefix=name,
1135 1138 matchfn=match,
1136 1139 subrepos=web.configbool("web", "archivesubrepos"))
1137 1140 return []
1138 1141
1139 1142
1140 1143 @webcommand('static')
1141 1144 def static(web, req, tmpl):
1142 1145 fname = req.form['file'][0]
1143 1146 # a repo owner may set web.static in .hg/hgrc to get any file
1144 1147 # readable by the user running the CGI script
1145 1148 static = web.config("web", "static", None, untrusted=False)
1146 1149 if not static:
1147 1150 tp = web.templatepath or templater.templatepaths()
1148 1151 if isinstance(tp, str):
1149 1152 tp = [tp]
1150 1153 static = [os.path.join(p, 'static') for p in tp]
1151 1154 staticfile(static, fname, req)
1152 1155 return []
1153 1156
1154 1157 @webcommand('graph')
1155 1158 def graph(web, req, tmpl):
1156 1159 """
1157 1160 /graph[/{revision}]
1158 1161 -------------------
1159 1162
1160 1163 Show information about the graphical topology of the repository.
1161 1164
1162 1165 Information rendered by this handler can be used to create visual
1163 1166 representations of repository topology.
1164 1167
1165 1168 The ``revision`` URL parameter controls the starting changeset.
1166 1169
1167 1170 The ``revcount`` query string argument can define the number of changesets
1168 1171 to show information for.
1169 1172
1170 1173 This handler will render the ``graph`` template.
1171 1174 """
1172 1175
1173 1176 if 'node' in req.form:
1174 1177 ctx = webutil.changectx(web.repo, req)
1175 1178 symrev = webutil.symrevorshortnode(req, ctx)
1176 1179 else:
1177 1180 ctx = web.repo['tip']
1178 1181 symrev = 'tip'
1179 1182 rev = ctx.rev()
1180 1183
1181 1184 bg_height = 39
1182 1185 revcount = web.maxshortchanges
1183 1186 if 'revcount' in req.form:
1184 1187 try:
1185 1188 revcount = int(req.form.get('revcount', [revcount])[0])
1186 1189 revcount = max(revcount, 1)
1187 1190 tmpl.defaults['sessionvars']['revcount'] = revcount
1188 1191 except ValueError:
1189 1192 pass
1190 1193
1191 1194 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1192 1195 lessvars['revcount'] = max(revcount / 2, 1)
1193 1196 morevars = copy.copy(tmpl.defaults['sessionvars'])
1194 1197 morevars['revcount'] = revcount * 2
1195 1198
1196 1199 count = len(web.repo)
1197 1200 pos = rev
1198 1201
1199 1202 uprev = min(max(0, count - 1), rev + revcount)
1200 1203 downrev = max(0, rev - revcount)
1201 1204 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1202 1205
1203 1206 tree = []
1204 1207 if pos != -1:
1205 1208 allrevs = web.repo.changelog.revs(pos, 0)
1206 1209 revs = []
1207 1210 for i in allrevs:
1208 1211 revs.append(i)
1209 1212 if len(revs) >= revcount:
1210 1213 break
1211 1214
1212 1215 # We have to feed a baseset to dagwalker as it is expecting smartset
1213 1216 # object. This does not have a big impact on hgweb performance itself
1214 1217 # since hgweb graphing code is not itself lazy yet.
1215 1218 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1216 1219 # As we said one line above... not lazy.
1217 1220 tree = list(graphmod.colored(dag, web.repo))
1218 1221
1219 1222 def getcolumns(tree):
1220 1223 cols = 0
1221 1224 for (id, type, ctx, vtx, edges) in tree:
1222 1225 if type != graphmod.CHANGESET:
1223 1226 continue
1224 1227 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1225 1228 max([edge[1] for edge in edges] or [0]))
1226 1229 return cols
1227 1230
1228 1231 def graphdata(usetuples, encodestr):
1229 1232 data = []
1230 1233
1231 1234 row = 0
1232 1235 for (id, type, ctx, vtx, edges) in tree:
1233 1236 if type != graphmod.CHANGESET:
1234 1237 continue
1235 1238 node = str(ctx)
1236 1239 age = encodestr(templatefilters.age(ctx.date()))
1237 1240 desc = templatefilters.firstline(encodestr(ctx.description()))
1238 1241 desc = cgi.escape(templatefilters.nonempty(desc))
1239 1242 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1240 1243 branch = cgi.escape(encodestr(ctx.branch()))
1241 1244 try:
1242 1245 branchnode = web.repo.branchtip(branch)
1243 1246 except error.RepoLookupError:
1244 1247 branchnode = None
1245 1248 branch = branch, branchnode == ctx.node()
1246 1249
1247 1250 if usetuples:
1248 1251 data.append((node, vtx, edges, desc, user, age, branch,
1249 1252 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1250 1253 [cgi.escape(encodestr(x))
1251 1254 for x in ctx.bookmarks()]))
1252 1255 else:
1253 1256 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1254 1257 'color': (edge[2] - 1) % 6 + 1,
1255 1258 'width': edge[3], 'bcolor': edge[4]}
1256 1259 for edge in edges]
1257 1260
1258 1261 data.append(
1259 1262 {'node': node,
1260 1263 'col': vtx[0],
1261 1264 'color': (vtx[1] - 1) % 6 + 1,
1262 1265 'edges': edgedata,
1263 1266 'row': row,
1264 1267 'nextrow': row + 1,
1265 1268 'desc': desc,
1266 1269 'user': user,
1267 1270 'age': age,
1268 1271 'bookmarks': webutil.nodebookmarksdict(
1269 1272 web.repo, ctx.node()),
1270 1273 'branches': webutil.nodebranchdict(web.repo, ctx),
1271 1274 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1272 1275 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1273 1276
1274 1277 row += 1
1275 1278
1276 1279 return data
1277 1280
1278 1281 cols = getcolumns(tree)
1279 1282 rows = len(tree)
1280 1283 canvasheight = (rows + 1) * bg_height - 27
1281 1284
1282 1285 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1283 1286 uprev=uprev,
1284 1287 lessvars=lessvars, morevars=morevars, downrev=downrev,
1285 1288 cols=cols, rows=rows,
1286 1289 canvaswidth=(cols + 1) * bg_height,
1287 1290 truecanvasheight=rows * bg_height,
1288 1291 canvasheight=canvasheight, bg_height=bg_height,
1289 1292 # {jsdata} will be passed to |json, so it must be in utf-8
1290 1293 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1291 1294 nodes=lambda **x: graphdata(False, str),
1292 1295 node=ctx.hex(), changenav=changenav)
1293 1296
1294 1297 def _getdoc(e):
1295 1298 doc = e[0].__doc__
1296 1299 if doc:
1297 1300 doc = _(doc).partition('\n')[0]
1298 1301 else:
1299 1302 doc = _('(no help text available)')
1300 1303 return doc
1301 1304
1302 1305 @webcommand('help')
1303 1306 def help(web, req, tmpl):
1304 1307 """
1305 1308 /help[/{topic}]
1306 1309 ---------------
1307 1310
1308 1311 Render help documentation.
1309 1312
1310 1313 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1311 1314 is defined, that help topic will be rendered. If not, an index of
1312 1315 available help topics will be rendered.
1313 1316
1314 1317 The ``help`` template will be rendered when requesting help for a topic.
1315 1318 ``helptopics`` will be rendered for the index of help topics.
1316 1319 """
1317 1320 from .. import commands, help as helpmod # avoid cycle
1318 1321
1319 1322 topicname = req.form.get('node', [None])[0]
1320 1323 if not topicname:
1321 1324 def topics(**map):
1322 1325 for entries, summary, _doc in helpmod.helptable:
1323 1326 yield {'topic': entries[0], 'summary': summary}
1324 1327
1325 1328 early, other = [], []
1326 1329 primary = lambda s: s.partition('|')[0]
1327 1330 for c, e in commands.table.iteritems():
1328 1331 doc = _getdoc(e)
1329 1332 if 'DEPRECATED' in doc or c.startswith('debug'):
1330 1333 continue
1331 1334 cmd = primary(c)
1332 1335 if cmd.startswith('^'):
1333 1336 early.append((cmd[1:], doc))
1334 1337 else:
1335 1338 other.append((cmd, doc))
1336 1339
1337 1340 early.sort()
1338 1341 other.sort()
1339 1342
1340 1343 def earlycommands(**map):
1341 1344 for c, doc in early:
1342 1345 yield {'topic': c, 'summary': doc}
1343 1346
1344 1347 def othercommands(**map):
1345 1348 for c, doc in other:
1346 1349 yield {'topic': c, 'summary': doc}
1347 1350
1348 1351 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1349 1352 othercommands=othercommands, title='Index')
1350 1353
1351 1354 # Render an index of sub-topics.
1352 1355 if topicname in helpmod.subtopics:
1353 1356 topics = []
1354 1357 for entries, summary, _doc in helpmod.subtopics[topicname]:
1355 1358 topics.append({
1356 1359 'topic': '%s.%s' % (topicname, entries[0]),
1357 1360 'basename': entries[0],
1358 1361 'summary': summary,
1359 1362 })
1360 1363
1361 1364 return tmpl('helptopics', topics=topics, title=topicname,
1362 1365 subindex=True)
1363 1366
1364 1367 u = webutil.wsgiui.load()
1365 1368 u.verbose = True
1366 1369
1367 1370 # Render a page from a sub-topic.
1368 1371 if '.' in topicname:
1369 1372 # TODO implement support for rendering sections, like
1370 1373 # `hg help` works.
1371 1374 topic, subtopic = topicname.split('.', 1)
1372 1375 if topic not in helpmod.subtopics:
1373 1376 raise ErrorResponse(HTTP_NOT_FOUND)
1374 1377 else:
1375 1378 topic = topicname
1376 1379 subtopic = None
1377 1380
1378 1381 try:
1379 1382 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1380 1383 except error.UnknownCommand:
1381 1384 raise ErrorResponse(HTTP_NOT_FOUND)
1382 1385 return tmpl('help', topic=topicname, doc=doc)
1383 1386
1384 1387 # tell hggettext to extract docstrings from these functions:
1385 1388 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now