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