##// END OF EJS Templates
configitems: drop redundant default of web.allow<archtype>...
Yuya Nishihara -
r34655:4182d206 default
parent child Browse files
Show More
@@ -1,1400 +1,1400 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')
96 guessmime = web.configbool('web', 'guessmime')
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')
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 ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
866 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
867 ``ignoreblanklines`` query string arguments have the same meaning as
867 ``ignoreblanklines`` query string arguments have the same meaning as
868 their ``[annotate]`` config equivalents. It uses the hgrc boolean
868 their ``[annotate]`` config equivalents. It uses the hgrc boolean
869 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
869 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
870 false and ``1`` and ``true`` are true. If not defined, the server
870 false and ``1`` and ``true`` are true. If not defined, the server
871 default settings are used.
871 default settings are used.
872
872
873 The ``fileannotate`` template is rendered.
873 The ``fileannotate`` template is rendered.
874 """
874 """
875 fctx = webutil.filectx(web.repo, req)
875 fctx = webutil.filectx(web.repo, req)
876 f = fctx.path()
876 f = fctx.path()
877 parity = paritygen(web.stripecount)
877 parity = paritygen(web.stripecount)
878 ishead = fctx.filerev() in fctx.filelog().headrevs()
878 ishead = fctx.filerev() in fctx.filelog().headrevs()
879
879
880 # parents() is called once per line and several lines likely belong to
880 # parents() is called once per line and several lines likely belong to
881 # same revision. So it is worth caching.
881 # same revision. So it is worth caching.
882 # TODO there are still redundant operations within basefilectx.parents()
882 # TODO there are still redundant operations within basefilectx.parents()
883 # and from the fctx.annotate() call itself that could be cached.
883 # and from the fctx.annotate() call itself that could be cached.
884 parentscache = {}
884 parentscache = {}
885 def parents(f):
885 def parents(f):
886 rev = f.rev()
886 rev = f.rev()
887 if rev not in parentscache:
887 if rev not in parentscache:
888 parentscache[rev] = []
888 parentscache[rev] = []
889 for p in f.parents():
889 for p in f.parents():
890 entry = {
890 entry = {
891 'node': p.hex(),
891 'node': p.hex(),
892 'rev': p.rev(),
892 'rev': p.rev(),
893 }
893 }
894 parentscache[rev].append(entry)
894 parentscache[rev].append(entry)
895
895
896 for p in parentscache[rev]:
896 for p in parentscache[rev]:
897 yield p
897 yield p
898
898
899 def annotate(**map):
899 def annotate(**map):
900 if fctx.isbinary():
900 if fctx.isbinary():
901 mt = (mimetypes.guess_type(fctx.path())[0]
901 mt = (mimetypes.guess_type(fctx.path())[0]
902 or 'application/octet-stream')
902 or 'application/octet-stream')
903 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
903 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
904 else:
904 else:
905 lines = webutil.annotate(req, fctx, web.repo.ui)
905 lines = webutil.annotate(req, fctx, web.repo.ui)
906
906
907 previousrev = None
907 previousrev = None
908 blockparitygen = paritygen(1)
908 blockparitygen = paritygen(1)
909 for lineno, (aline, l) in enumerate(lines):
909 for lineno, (aline, l) in enumerate(lines):
910 f = aline.fctx
910 f = aline.fctx
911 rev = f.rev()
911 rev = f.rev()
912 if rev != previousrev:
912 if rev != previousrev:
913 blockhead = True
913 blockhead = True
914 blockparity = next(blockparitygen)
914 blockparity = next(blockparitygen)
915 else:
915 else:
916 blockhead = None
916 blockhead = None
917 previousrev = rev
917 previousrev = rev
918 yield {"parity": next(parity),
918 yield {"parity": next(parity),
919 "node": f.hex(),
919 "node": f.hex(),
920 "rev": rev,
920 "rev": rev,
921 "author": f.user(),
921 "author": f.user(),
922 "parents": parents(f),
922 "parents": parents(f),
923 "desc": f.description(),
923 "desc": f.description(),
924 "extra": f.extra(),
924 "extra": f.extra(),
925 "file": f.path(),
925 "file": f.path(),
926 "blockhead": blockhead,
926 "blockhead": blockhead,
927 "blockparity": blockparity,
927 "blockparity": blockparity,
928 "targetline": aline.lineno,
928 "targetline": aline.lineno,
929 "line": l,
929 "line": l,
930 "lineno": lineno + 1,
930 "lineno": lineno + 1,
931 "lineid": "l%d" % (lineno + 1),
931 "lineid": "l%d" % (lineno + 1),
932 "linenumber": "% 6d" % (lineno + 1),
932 "linenumber": "% 6d" % (lineno + 1),
933 "revdate": f.date()}
933 "revdate": f.date()}
934
934
935 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
935 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
936 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
936 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
937
937
938 return tmpl("fileannotate",
938 return tmpl("fileannotate",
939 file=f,
939 file=f,
940 annotate=annotate,
940 annotate=annotate,
941 path=webutil.up(f),
941 path=webutil.up(f),
942 symrev=webutil.symrevorshortnode(req, fctx),
942 symrev=webutil.symrevorshortnode(req, fctx),
943 rename=webutil.renamelink(fctx),
943 rename=webutil.renamelink(fctx),
944 permissions=fctx.manifest().flags(f),
944 permissions=fctx.manifest().flags(f),
945 ishead=int(ishead),
945 ishead=int(ishead),
946 diffopts=diffopts,
946 diffopts=diffopts,
947 **webutil.commonentry(web.repo, fctx))
947 **webutil.commonentry(web.repo, fctx))
948
948
949 @webcommand('filelog')
949 @webcommand('filelog')
950 def filelog(web, req, tmpl):
950 def filelog(web, req, tmpl):
951 """
951 """
952 /filelog/{revision}/{path}
952 /filelog/{revision}/{path}
953 --------------------------
953 --------------------------
954
954
955 Show information about the history of a file in the repository.
955 Show information about the history of a file in the repository.
956
956
957 The ``revcount`` query string argument can be defined to control the
957 The ``revcount`` query string argument can be defined to control the
958 maximum number of entries to show.
958 maximum number of entries to show.
959
959
960 The ``filelog`` template will be rendered.
960 The ``filelog`` template will be rendered.
961 """
961 """
962
962
963 try:
963 try:
964 fctx = webutil.filectx(web.repo, req)
964 fctx = webutil.filectx(web.repo, req)
965 f = fctx.path()
965 f = fctx.path()
966 fl = fctx.filelog()
966 fl = fctx.filelog()
967 except error.LookupError:
967 except error.LookupError:
968 f = webutil.cleanpath(web.repo, req.form['file'][0])
968 f = webutil.cleanpath(web.repo, req.form['file'][0])
969 fl = web.repo.file(f)
969 fl = web.repo.file(f)
970 numrevs = len(fl)
970 numrevs = len(fl)
971 if not numrevs: # file doesn't exist at all
971 if not numrevs: # file doesn't exist at all
972 raise
972 raise
973 rev = webutil.changectx(web.repo, req).rev()
973 rev = webutil.changectx(web.repo, req).rev()
974 first = fl.linkrev(0)
974 first = fl.linkrev(0)
975 if rev < first: # current rev is from before file existed
975 if rev < first: # current rev is from before file existed
976 raise
976 raise
977 frev = numrevs - 1
977 frev = numrevs - 1
978 while fl.linkrev(frev) > rev:
978 while fl.linkrev(frev) > rev:
979 frev -= 1
979 frev -= 1
980 fctx = web.repo.filectx(f, fl.linkrev(frev))
980 fctx = web.repo.filectx(f, fl.linkrev(frev))
981
981
982 revcount = web.maxshortchanges
982 revcount = web.maxshortchanges
983 if 'revcount' in req.form:
983 if 'revcount' in req.form:
984 try:
984 try:
985 revcount = int(req.form.get('revcount', [revcount])[0])
985 revcount = int(req.form.get('revcount', [revcount])[0])
986 revcount = max(revcount, 1)
986 revcount = max(revcount, 1)
987 tmpl.defaults['sessionvars']['revcount'] = revcount
987 tmpl.defaults['sessionvars']['revcount'] = revcount
988 except ValueError:
988 except ValueError:
989 pass
989 pass
990
990
991 lrange = webutil.linerange(req)
991 lrange = webutil.linerange(req)
992
992
993 lessvars = copy.copy(tmpl.defaults['sessionvars'])
993 lessvars = copy.copy(tmpl.defaults['sessionvars'])
994 lessvars['revcount'] = max(revcount / 2, 1)
994 lessvars['revcount'] = max(revcount / 2, 1)
995 morevars = copy.copy(tmpl.defaults['sessionvars'])
995 morevars = copy.copy(tmpl.defaults['sessionvars'])
996 morevars['revcount'] = revcount * 2
996 morevars['revcount'] = revcount * 2
997
997
998 patch = 'patch' in req.form
998 patch = 'patch' in req.form
999 if patch:
999 if patch:
1000 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
1000 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
1001 descend = 'descend' in req.form
1001 descend = 'descend' in req.form
1002 if descend:
1002 if descend:
1003 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
1003 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
1004
1004
1005 count = fctx.filerev() + 1
1005 count = fctx.filerev() + 1
1006 start = max(0, count - revcount) # first rev on this page
1006 start = max(0, count - revcount) # first rev on this page
1007 end = min(count, start + revcount) # last rev on this page
1007 end = min(count, start + revcount) # last rev on this page
1008 parity = paritygen(web.stripecount, offset=start - end)
1008 parity = paritygen(web.stripecount, offset=start - end)
1009
1009
1010 repo = web.repo
1010 repo = web.repo
1011 revs = fctx.filelog().revs(start, end - 1)
1011 revs = fctx.filelog().revs(start, end - 1)
1012 entries = []
1012 entries = []
1013
1013
1014 diffstyle = web.config('web', 'style')
1014 diffstyle = web.config('web', 'style')
1015 if 'style' in req.form:
1015 if 'style' in req.form:
1016 diffstyle = req.form['style'][0]
1016 diffstyle = req.form['style'][0]
1017
1017
1018 def diff(fctx, linerange=None):
1018 def diff(fctx, linerange=None):
1019 ctx = fctx.changectx()
1019 ctx = fctx.changectx()
1020 basectx = ctx.p1()
1020 basectx = ctx.p1()
1021 path = fctx.path()
1021 path = fctx.path()
1022 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1022 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1023 linerange=linerange,
1023 linerange=linerange,
1024 lineidprefix='%s-' % ctx.hex()[:12])
1024 lineidprefix='%s-' % ctx.hex()[:12])
1025
1025
1026 linerange = None
1026 linerange = None
1027 if lrange is not None:
1027 if lrange is not None:
1028 linerange = webutil.formatlinerange(*lrange)
1028 linerange = webutil.formatlinerange(*lrange)
1029 # deactivate numeric nav links when linerange is specified as this
1029 # deactivate numeric nav links when linerange is specified as this
1030 # would required a dedicated "revnav" class
1030 # would required a dedicated "revnav" class
1031 nav = None
1031 nav = None
1032 if descend:
1032 if descend:
1033 it = dagop.blockdescendants(fctx, *lrange)
1033 it = dagop.blockdescendants(fctx, *lrange)
1034 else:
1034 else:
1035 it = dagop.blockancestors(fctx, *lrange)
1035 it = dagop.blockancestors(fctx, *lrange)
1036 for i, (c, lr) in enumerate(it, 1):
1036 for i, (c, lr) in enumerate(it, 1):
1037 diffs = None
1037 diffs = None
1038 if patch:
1038 if patch:
1039 diffs = diff(c, linerange=lr)
1039 diffs = diff(c, linerange=lr)
1040 # follow renames accross filtered (not in range) revisions
1040 # follow renames accross filtered (not in range) revisions
1041 path = c.path()
1041 path = c.path()
1042 entries.append(dict(
1042 entries.append(dict(
1043 parity=next(parity),
1043 parity=next(parity),
1044 filerev=c.rev(),
1044 filerev=c.rev(),
1045 file=path,
1045 file=path,
1046 diff=diffs,
1046 diff=diffs,
1047 linerange=webutil.formatlinerange(*lr),
1047 linerange=webutil.formatlinerange(*lr),
1048 **webutil.commonentry(repo, c)))
1048 **webutil.commonentry(repo, c)))
1049 if i == revcount:
1049 if i == revcount:
1050 break
1050 break
1051 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1051 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1052 morevars['linerange'] = lessvars['linerange']
1052 morevars['linerange'] = lessvars['linerange']
1053 else:
1053 else:
1054 for i in revs:
1054 for i in revs:
1055 iterfctx = fctx.filectx(i)
1055 iterfctx = fctx.filectx(i)
1056 diffs = None
1056 diffs = None
1057 if patch:
1057 if patch:
1058 diffs = diff(iterfctx)
1058 diffs = diff(iterfctx)
1059 entries.append(dict(
1059 entries.append(dict(
1060 parity=next(parity),
1060 parity=next(parity),
1061 filerev=i,
1061 filerev=i,
1062 file=f,
1062 file=f,
1063 diff=diffs,
1063 diff=diffs,
1064 rename=webutil.renamelink(iterfctx),
1064 rename=webutil.renamelink(iterfctx),
1065 **webutil.commonentry(repo, iterfctx)))
1065 **webutil.commonentry(repo, iterfctx)))
1066 entries.reverse()
1066 entries.reverse()
1067 revnav = webutil.filerevnav(web.repo, fctx.path())
1067 revnav = webutil.filerevnav(web.repo, fctx.path())
1068 nav = revnav.gen(end - 1, revcount, count)
1068 nav = revnav.gen(end - 1, revcount, count)
1069
1069
1070 latestentry = entries[:1]
1070 latestentry = entries[:1]
1071
1071
1072 return tmpl("filelog",
1072 return tmpl("filelog",
1073 file=f,
1073 file=f,
1074 nav=nav,
1074 nav=nav,
1075 symrev=webutil.symrevorshortnode(req, fctx),
1075 symrev=webutil.symrevorshortnode(req, fctx),
1076 entries=entries,
1076 entries=entries,
1077 descend=descend,
1077 descend=descend,
1078 patch=patch,
1078 patch=patch,
1079 latestentry=latestentry,
1079 latestentry=latestentry,
1080 linerange=linerange,
1080 linerange=linerange,
1081 revcount=revcount,
1081 revcount=revcount,
1082 morevars=morevars,
1082 morevars=morevars,
1083 lessvars=lessvars,
1083 lessvars=lessvars,
1084 **webutil.commonentry(web.repo, fctx))
1084 **webutil.commonentry(web.repo, fctx))
1085
1085
1086 @webcommand('archive')
1086 @webcommand('archive')
1087 def archive(web, req, tmpl):
1087 def archive(web, req, tmpl):
1088 """
1088 """
1089 /archive/{revision}.{format}[/{path}]
1089 /archive/{revision}.{format}[/{path}]
1090 -------------------------------------
1090 -------------------------------------
1091
1091
1092 Obtain an archive of repository content.
1092 Obtain an archive of repository content.
1093
1093
1094 The content and type of the archive is defined by a URL path parameter.
1094 The content and type of the archive is defined by a URL path parameter.
1095 ``format`` is the file extension of the archive type to be generated. e.g.
1095 ``format`` is the file extension of the archive type to be generated. e.g.
1096 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1096 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1097 server configuration.
1097 server configuration.
1098
1098
1099 The optional ``path`` URL parameter controls content to include in the
1099 The optional ``path`` URL parameter controls content to include in the
1100 archive. If omitted, every file in the specified revision is present in the
1100 archive. If omitted, every file in the specified revision is present in the
1101 archive. If included, only the specified file or contents of the specified
1101 archive. If included, only the specified file or contents of the specified
1102 directory will be included in the archive.
1102 directory will be included in the archive.
1103
1103
1104 No template is used for this handler. Raw, binary content is generated.
1104 No template is used for this handler. Raw, binary content is generated.
1105 """
1105 """
1106
1106
1107 type_ = req.form.get('type', [None])[0]
1107 type_ = req.form.get('type', [None])[0]
1108 allowed = web.configlist("web", "allow_archive")
1108 allowed = web.configlist("web", "allow_archive")
1109 key = req.form['node'][0]
1109 key = req.form['node'][0]
1110
1110
1111 if type_ not in web.archivespecs:
1111 if type_ not in web.archivespecs:
1112 msg = 'Unsupported archive type: %s' % type_
1112 msg = 'Unsupported archive type: %s' % type_
1113 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1113 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1114
1114
1115 if not ((type_ in allowed or
1115 if not ((type_ in allowed or
1116 web.configbool("web", "allow" + type_, False))):
1116 web.configbool("web", "allow" + type_))):
1117 msg = 'Archive type not allowed: %s' % type_
1117 msg = 'Archive type not allowed: %s' % type_
1118 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1118 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1119
1119
1120 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1120 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1121 cnode = web.repo.lookup(key)
1121 cnode = web.repo.lookup(key)
1122 arch_version = key
1122 arch_version = key
1123 if cnode == key or key == 'tip':
1123 if cnode == key or key == 'tip':
1124 arch_version = short(cnode)
1124 arch_version = short(cnode)
1125 name = "%s-%s" % (reponame, arch_version)
1125 name = "%s-%s" % (reponame, arch_version)
1126
1126
1127 ctx = webutil.changectx(web.repo, req)
1127 ctx = webutil.changectx(web.repo, req)
1128 pats = []
1128 pats = []
1129 match = scmutil.match(ctx, [])
1129 match = scmutil.match(ctx, [])
1130 file = req.form.get('file', None)
1130 file = req.form.get('file', None)
1131 if file:
1131 if file:
1132 pats = ['path:' + file[0]]
1132 pats = ['path:' + file[0]]
1133 match = scmutil.match(ctx, pats, default='path')
1133 match = scmutil.match(ctx, pats, default='path')
1134 if pats:
1134 if pats:
1135 files = [f for f in ctx.manifest().keys() if match(f)]
1135 files = [f for f in ctx.manifest().keys() if match(f)]
1136 if not files:
1136 if not files:
1137 raise ErrorResponse(HTTP_NOT_FOUND,
1137 raise ErrorResponse(HTTP_NOT_FOUND,
1138 'file(s) not found: %s' % file[0])
1138 'file(s) not found: %s' % file[0])
1139
1139
1140 mimetype, artype, extension, encoding = web.archivespecs[type_]
1140 mimetype, artype, extension, encoding = web.archivespecs[type_]
1141 headers = [
1141 headers = [
1142 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1142 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1143 ]
1143 ]
1144 if encoding:
1144 if encoding:
1145 headers.append(('Content-Encoding', encoding))
1145 headers.append(('Content-Encoding', encoding))
1146 req.headers.extend(headers)
1146 req.headers.extend(headers)
1147 req.respond(HTTP_OK, mimetype)
1147 req.respond(HTTP_OK, mimetype)
1148
1148
1149 archival.archive(web.repo, req, cnode, artype, prefix=name,
1149 archival.archive(web.repo, req, cnode, artype, prefix=name,
1150 matchfn=match,
1150 matchfn=match,
1151 subrepos=web.configbool("web", "archivesubrepos"))
1151 subrepos=web.configbool("web", "archivesubrepos"))
1152 return []
1152 return []
1153
1153
1154
1154
1155 @webcommand('static')
1155 @webcommand('static')
1156 def static(web, req, tmpl):
1156 def static(web, req, tmpl):
1157 fname = req.form['file'][0]
1157 fname = req.form['file'][0]
1158 # a repo owner may set web.static in .hg/hgrc to get any file
1158 # a repo owner may set web.static in .hg/hgrc to get any file
1159 # readable by the user running the CGI script
1159 # readable by the user running the CGI script
1160 static = web.config("web", "static", None, untrusted=False)
1160 static = web.config("web", "static", None, untrusted=False)
1161 if not static:
1161 if not static:
1162 tp = web.templatepath or templater.templatepaths()
1162 tp = web.templatepath or templater.templatepaths()
1163 if isinstance(tp, str):
1163 if isinstance(tp, str):
1164 tp = [tp]
1164 tp = [tp]
1165 static = [os.path.join(p, 'static') for p in tp]
1165 static = [os.path.join(p, 'static') for p in tp]
1166 staticfile(static, fname, req)
1166 staticfile(static, fname, req)
1167 return []
1167 return []
1168
1168
1169 @webcommand('graph')
1169 @webcommand('graph')
1170 def graph(web, req, tmpl):
1170 def graph(web, req, tmpl):
1171 """
1171 """
1172 /graph[/{revision}]
1172 /graph[/{revision}]
1173 -------------------
1173 -------------------
1174
1174
1175 Show information about the graphical topology of the repository.
1175 Show information about the graphical topology of the repository.
1176
1176
1177 Information rendered by this handler can be used to create visual
1177 Information rendered by this handler can be used to create visual
1178 representations of repository topology.
1178 representations of repository topology.
1179
1179
1180 The ``revision`` URL parameter controls the starting changeset.
1180 The ``revision`` URL parameter controls the starting changeset.
1181
1181
1182 The ``revcount`` query string argument can define the number of changesets
1182 The ``revcount`` query string argument can define the number of changesets
1183 to show information for.
1183 to show information for.
1184
1184
1185 This handler will render the ``graph`` template.
1185 This handler will render the ``graph`` template.
1186 """
1186 """
1187
1187
1188 if 'node' in req.form:
1188 if 'node' in req.form:
1189 ctx = webutil.changectx(web.repo, req)
1189 ctx = webutil.changectx(web.repo, req)
1190 symrev = webutil.symrevorshortnode(req, ctx)
1190 symrev = webutil.symrevorshortnode(req, ctx)
1191 else:
1191 else:
1192 ctx = web.repo['tip']
1192 ctx = web.repo['tip']
1193 symrev = 'tip'
1193 symrev = 'tip'
1194 rev = ctx.rev()
1194 rev = ctx.rev()
1195
1195
1196 bg_height = 39
1196 bg_height = 39
1197 revcount = web.maxshortchanges
1197 revcount = web.maxshortchanges
1198 if 'revcount' in req.form:
1198 if 'revcount' in req.form:
1199 try:
1199 try:
1200 revcount = int(req.form.get('revcount', [revcount])[0])
1200 revcount = int(req.form.get('revcount', [revcount])[0])
1201 revcount = max(revcount, 1)
1201 revcount = max(revcount, 1)
1202 tmpl.defaults['sessionvars']['revcount'] = revcount
1202 tmpl.defaults['sessionvars']['revcount'] = revcount
1203 except ValueError:
1203 except ValueError:
1204 pass
1204 pass
1205
1205
1206 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1206 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1207 lessvars['revcount'] = max(revcount / 2, 1)
1207 lessvars['revcount'] = max(revcount / 2, 1)
1208 morevars = copy.copy(tmpl.defaults['sessionvars'])
1208 morevars = copy.copy(tmpl.defaults['sessionvars'])
1209 morevars['revcount'] = revcount * 2
1209 morevars['revcount'] = revcount * 2
1210
1210
1211 count = len(web.repo)
1211 count = len(web.repo)
1212 pos = rev
1212 pos = rev
1213
1213
1214 uprev = min(max(0, count - 1), rev + revcount)
1214 uprev = min(max(0, count - 1), rev + revcount)
1215 downrev = max(0, rev - revcount)
1215 downrev = max(0, rev - revcount)
1216 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1216 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1217
1217
1218 tree = []
1218 tree = []
1219 if pos != -1:
1219 if pos != -1:
1220 allrevs = web.repo.changelog.revs(pos, 0)
1220 allrevs = web.repo.changelog.revs(pos, 0)
1221 revs = []
1221 revs = []
1222 for i in allrevs:
1222 for i in allrevs:
1223 revs.append(i)
1223 revs.append(i)
1224 if len(revs) >= revcount:
1224 if len(revs) >= revcount:
1225 break
1225 break
1226
1226
1227 # We have to feed a baseset to dagwalker as it is expecting smartset
1227 # We have to feed a baseset to dagwalker as it is expecting smartset
1228 # object. This does not have a big impact on hgweb performance itself
1228 # object. This does not have a big impact on hgweb performance itself
1229 # since hgweb graphing code is not itself lazy yet.
1229 # since hgweb graphing code is not itself lazy yet.
1230 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1230 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1231 # As we said one line above... not lazy.
1231 # As we said one line above... not lazy.
1232 tree = list(graphmod.colored(dag, web.repo))
1232 tree = list(graphmod.colored(dag, web.repo))
1233
1233
1234 def getcolumns(tree):
1234 def getcolumns(tree):
1235 cols = 0
1235 cols = 0
1236 for (id, type, ctx, vtx, edges) in tree:
1236 for (id, type, ctx, vtx, edges) in tree:
1237 if type != graphmod.CHANGESET:
1237 if type != graphmod.CHANGESET:
1238 continue
1238 continue
1239 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1239 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1240 max([edge[1] for edge in edges] or [0]))
1240 max([edge[1] for edge in edges] or [0]))
1241 return cols
1241 return cols
1242
1242
1243 def graphdata(usetuples, encodestr):
1243 def graphdata(usetuples, encodestr):
1244 data = []
1244 data = []
1245
1245
1246 row = 0
1246 row = 0
1247 for (id, type, ctx, vtx, edges) in tree:
1247 for (id, type, ctx, vtx, edges) in tree:
1248 if type != graphmod.CHANGESET:
1248 if type != graphmod.CHANGESET:
1249 continue
1249 continue
1250 node = str(ctx)
1250 node = str(ctx)
1251 age = encodestr(templatefilters.age(ctx.date()))
1251 age = encodestr(templatefilters.age(ctx.date()))
1252 desc = templatefilters.firstline(encodestr(ctx.description()))
1252 desc = templatefilters.firstline(encodestr(ctx.description()))
1253 desc = cgi.escape(templatefilters.nonempty(desc))
1253 desc = cgi.escape(templatefilters.nonempty(desc))
1254 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1254 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1255 branch = cgi.escape(encodestr(ctx.branch()))
1255 branch = cgi.escape(encodestr(ctx.branch()))
1256 try:
1256 try:
1257 branchnode = web.repo.branchtip(branch)
1257 branchnode = web.repo.branchtip(branch)
1258 except error.RepoLookupError:
1258 except error.RepoLookupError:
1259 branchnode = None
1259 branchnode = None
1260 branch = branch, branchnode == ctx.node()
1260 branch = branch, branchnode == ctx.node()
1261
1261
1262 if usetuples:
1262 if usetuples:
1263 data.append((node, vtx, edges, desc, user, age, branch,
1263 data.append((node, vtx, edges, desc, user, age, branch,
1264 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1264 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1265 [cgi.escape(encodestr(x))
1265 [cgi.escape(encodestr(x))
1266 for x in ctx.bookmarks()]))
1266 for x in ctx.bookmarks()]))
1267 else:
1267 else:
1268 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1268 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1269 'color': (edge[2] - 1) % 6 + 1,
1269 'color': (edge[2] - 1) % 6 + 1,
1270 'width': edge[3], 'bcolor': edge[4]}
1270 'width': edge[3], 'bcolor': edge[4]}
1271 for edge in edges]
1271 for edge in edges]
1272
1272
1273 data.append(
1273 data.append(
1274 {'node': node,
1274 {'node': node,
1275 'col': vtx[0],
1275 'col': vtx[0],
1276 'color': (vtx[1] - 1) % 6 + 1,
1276 'color': (vtx[1] - 1) % 6 + 1,
1277 'edges': edgedata,
1277 'edges': edgedata,
1278 'row': row,
1278 'row': row,
1279 'nextrow': row + 1,
1279 'nextrow': row + 1,
1280 'desc': desc,
1280 'desc': desc,
1281 'user': user,
1281 'user': user,
1282 'age': age,
1282 'age': age,
1283 'bookmarks': webutil.nodebookmarksdict(
1283 'bookmarks': webutil.nodebookmarksdict(
1284 web.repo, ctx.node()),
1284 web.repo, ctx.node()),
1285 'branches': webutil.nodebranchdict(web.repo, ctx),
1285 'branches': webutil.nodebranchdict(web.repo, ctx),
1286 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1286 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1287 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1287 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1288
1288
1289 row += 1
1289 row += 1
1290
1290
1291 return data
1291 return data
1292
1292
1293 cols = getcolumns(tree)
1293 cols = getcolumns(tree)
1294 rows = len(tree)
1294 rows = len(tree)
1295 canvasheight = (rows + 1) * bg_height - 27
1295 canvasheight = (rows + 1) * bg_height - 27
1296
1296
1297 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1297 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1298 uprev=uprev,
1298 uprev=uprev,
1299 lessvars=lessvars, morevars=morevars, downrev=downrev,
1299 lessvars=lessvars, morevars=morevars, downrev=downrev,
1300 cols=cols, rows=rows,
1300 cols=cols, rows=rows,
1301 canvaswidth=(cols + 1) * bg_height,
1301 canvaswidth=(cols + 1) * bg_height,
1302 truecanvasheight=rows * bg_height,
1302 truecanvasheight=rows * bg_height,
1303 canvasheight=canvasheight, bg_height=bg_height,
1303 canvasheight=canvasheight, bg_height=bg_height,
1304 # {jsdata} will be passed to |json, so it must be in utf-8
1304 # {jsdata} will be passed to |json, so it must be in utf-8
1305 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1305 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1306 nodes=lambda **x: graphdata(False, str),
1306 nodes=lambda **x: graphdata(False, str),
1307 node=ctx.hex(), changenav=changenav)
1307 node=ctx.hex(), changenav=changenav)
1308
1308
1309 def _getdoc(e):
1309 def _getdoc(e):
1310 doc = e[0].__doc__
1310 doc = e[0].__doc__
1311 if doc:
1311 if doc:
1312 doc = _(doc).partition('\n')[0]
1312 doc = _(doc).partition('\n')[0]
1313 else:
1313 else:
1314 doc = _('(no help text available)')
1314 doc = _('(no help text available)')
1315 return doc
1315 return doc
1316
1316
1317 @webcommand('help')
1317 @webcommand('help')
1318 def help(web, req, tmpl):
1318 def help(web, req, tmpl):
1319 """
1319 """
1320 /help[/{topic}]
1320 /help[/{topic}]
1321 ---------------
1321 ---------------
1322
1322
1323 Render help documentation.
1323 Render help documentation.
1324
1324
1325 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1325 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1326 is defined, that help topic will be rendered. If not, an index of
1326 is defined, that help topic will be rendered. If not, an index of
1327 available help topics will be rendered.
1327 available help topics will be rendered.
1328
1328
1329 The ``help`` template will be rendered when requesting help for a topic.
1329 The ``help`` template will be rendered when requesting help for a topic.
1330 ``helptopics`` will be rendered for the index of help topics.
1330 ``helptopics`` will be rendered for the index of help topics.
1331 """
1331 """
1332 from .. import commands, help as helpmod # avoid cycle
1332 from .. import commands, help as helpmod # avoid cycle
1333
1333
1334 topicname = req.form.get('node', [None])[0]
1334 topicname = req.form.get('node', [None])[0]
1335 if not topicname:
1335 if not topicname:
1336 def topics(**map):
1336 def topics(**map):
1337 for entries, summary, _doc in helpmod.helptable:
1337 for entries, summary, _doc in helpmod.helptable:
1338 yield {'topic': entries[0], 'summary': summary}
1338 yield {'topic': entries[0], 'summary': summary}
1339
1339
1340 early, other = [], []
1340 early, other = [], []
1341 primary = lambda s: s.partition('|')[0]
1341 primary = lambda s: s.partition('|')[0]
1342 for c, e in commands.table.iteritems():
1342 for c, e in commands.table.iteritems():
1343 doc = _getdoc(e)
1343 doc = _getdoc(e)
1344 if 'DEPRECATED' in doc or c.startswith('debug'):
1344 if 'DEPRECATED' in doc or c.startswith('debug'):
1345 continue
1345 continue
1346 cmd = primary(c)
1346 cmd = primary(c)
1347 if cmd.startswith('^'):
1347 if cmd.startswith('^'):
1348 early.append((cmd[1:], doc))
1348 early.append((cmd[1:], doc))
1349 else:
1349 else:
1350 other.append((cmd, doc))
1350 other.append((cmd, doc))
1351
1351
1352 early.sort()
1352 early.sort()
1353 other.sort()
1353 other.sort()
1354
1354
1355 def earlycommands(**map):
1355 def earlycommands(**map):
1356 for c, doc in early:
1356 for c, doc in early:
1357 yield {'topic': c, 'summary': doc}
1357 yield {'topic': c, 'summary': doc}
1358
1358
1359 def othercommands(**map):
1359 def othercommands(**map):
1360 for c, doc in other:
1360 for c, doc in other:
1361 yield {'topic': c, 'summary': doc}
1361 yield {'topic': c, 'summary': doc}
1362
1362
1363 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1363 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1364 othercommands=othercommands, title='Index')
1364 othercommands=othercommands, title='Index')
1365
1365
1366 # Render an index of sub-topics.
1366 # Render an index of sub-topics.
1367 if topicname in helpmod.subtopics:
1367 if topicname in helpmod.subtopics:
1368 topics = []
1368 topics = []
1369 for entries, summary, _doc in helpmod.subtopics[topicname]:
1369 for entries, summary, _doc in helpmod.subtopics[topicname]:
1370 topics.append({
1370 topics.append({
1371 'topic': '%s.%s' % (topicname, entries[0]),
1371 'topic': '%s.%s' % (topicname, entries[0]),
1372 'basename': entries[0],
1372 'basename': entries[0],
1373 'summary': summary,
1373 'summary': summary,
1374 })
1374 })
1375
1375
1376 return tmpl('helptopics', topics=topics, title=topicname,
1376 return tmpl('helptopics', topics=topics, title=topicname,
1377 subindex=True)
1377 subindex=True)
1378
1378
1379 u = webutil.wsgiui.load()
1379 u = webutil.wsgiui.load()
1380 u.verbose = True
1380 u.verbose = True
1381
1381
1382 # Render a page from a sub-topic.
1382 # Render a page from a sub-topic.
1383 if '.' in topicname:
1383 if '.' in topicname:
1384 # TODO implement support for rendering sections, like
1384 # TODO implement support for rendering sections, like
1385 # `hg help` works.
1385 # `hg help` works.
1386 topic, subtopic = topicname.split('.', 1)
1386 topic, subtopic = topicname.split('.', 1)
1387 if topic not in helpmod.subtopics:
1387 if topic not in helpmod.subtopics:
1388 raise ErrorResponse(HTTP_NOT_FOUND)
1388 raise ErrorResponse(HTTP_NOT_FOUND)
1389 else:
1389 else:
1390 topic = topicname
1390 topic = topicname
1391 subtopic = None
1391 subtopic = None
1392
1392
1393 try:
1393 try:
1394 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1394 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1395 except error.UnknownCommand:
1395 except error.UnknownCommand:
1396 raise ErrorResponse(HTTP_NOT_FOUND)
1396 raise ErrorResponse(HTTP_NOT_FOUND)
1397 return tmpl('help', topic=topicname, doc=doc)
1397 return tmpl('help', topic=topicname, doc=doc)
1398
1398
1399 # tell hggettext to extract docstrings from these functions:
1399 # tell hggettext to extract docstrings from these functions:
1400 i18nfunctions = commands.values()
1400 i18nfunctions = commands.values()
@@ -1,373 +1,403 b''
1 #require serve
1 #require serve
2
2
3 $ hg init test
3 $ hg init test
4 $ cd test
4 $ cd test
5 $ echo foo>foo
5 $ echo foo>foo
6 $ hg commit -Am 1 -d '1 0'
6 $ hg commit -Am 1 -d '1 0'
7 adding foo
7 adding foo
8 $ echo bar>bar
8 $ echo bar>bar
9 $ hg commit -Am 2 -d '2 0'
9 $ hg commit -Am 2 -d '2 0'
10 adding bar
10 adding bar
11 $ mkdir baz
11 $ mkdir baz
12 $ echo bletch>baz/bletch
12 $ echo bletch>baz/bletch
13 $ hg commit -Am 3 -d '1000000000 0'
13 $ hg commit -Am 3 -d '1000000000 0'
14 adding baz/bletch
14 adding baz/bletch
15 $ hg init subrepo
15 $ hg init subrepo
16 $ touch subrepo/sub
16 $ touch subrepo/sub
17 $ hg -q -R subrepo ci -Am "init subrepo"
17 $ hg -q -R subrepo ci -Am "init subrepo"
18 $ echo "subrepo = subrepo" > .hgsub
18 $ echo "subrepo = subrepo" > .hgsub
19 $ hg add .hgsub
19 $ hg add .hgsub
20 $ hg ci -m "add subrepo"
20 $ hg ci -m "add subrepo"
21 $ echo "[web]" >> .hg/hgrc
21 $ echo "[web]" >> .hg/hgrc
22 $ echo "name = test-archive" >> .hg/hgrc
22 $ echo "name = test-archive" >> .hg/hgrc
23 $ echo "archivesubrepos = True" >> .hg/hgrc
23 $ echo "archivesubrepos = True" >> .hg/hgrc
24 $ cp .hg/hgrc .hg/hgrc-base
24 $ cp .hg/hgrc .hg/hgrc-base
25 > test_archtype() {
25 > test_archtype() {
26 > echo "allow_archive = $1" >> .hg/hgrc
26 > echo "allow_archive = $1" >> .hg/hgrc
27 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
27 > test_archtype_run "$@"
28 > }
29 > test_archtype_deprecated() {
30 > echo "allow$1 = True" >> .hg/hgrc
31 > test_archtype_run "$@"
32 > }
33 > test_archtype_run() {
34 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log \
35 > --config extensions.blackbox= --config blackbox.track=develwarn
28 > cat hg.pid >> $DAEMON_PIDS
36 > cat hg.pid >> $DAEMON_PIDS
29 > echo % $1 allowed should give 200
37 > echo % $1 allowed should give 200
30 > get-with-headers.py localhost:$HGPORT "archive/tip.$2" | head -n 1
38 > get-with-headers.py localhost:$HGPORT "archive/tip.$2" | head -n 1
31 > echo % $3 and $4 disallowed should both give 403
39 > echo % $3 and $4 disallowed should both give 403
32 > get-with-headers.py localhost:$HGPORT "archive/tip.$3" | head -n 1
40 > get-with-headers.py localhost:$HGPORT "archive/tip.$3" | head -n 1
33 > get-with-headers.py localhost:$HGPORT "archive/tip.$4" | head -n 1
41 > get-with-headers.py localhost:$HGPORT "archive/tip.$4" | head -n 1
34 > killdaemons.py
42 > killdaemons.py
35 > cat errors.log
43 > cat errors.log
44 > hg blackbox --config extensions.blackbox= --config blackbox.track=
36 > cp .hg/hgrc-base .hg/hgrc
45 > cp .hg/hgrc-base .hg/hgrc
37 > }
46 > }
38
47
39 check http return codes
48 check http return codes
40
49
41 $ test_archtype gz tar.gz tar.bz2 zip
50 $ test_archtype gz tar.gz tar.bz2 zip
42 % gz allowed should give 200
51 % gz allowed should give 200
43 200 Script output follows
52 200 Script output follows
44 % tar.bz2 and zip disallowed should both give 403
53 % tar.bz2 and zip disallowed should both give 403
45 403 Archive type not allowed: bz2
54 403 Archive type not allowed: bz2
46 403 Archive type not allowed: zip
55 403 Archive type not allowed: zip
47 $ test_archtype bz2 tar.bz2 zip tar.gz
56 $ test_archtype bz2 tar.bz2 zip tar.gz
48 % bz2 allowed should give 200
57 % bz2 allowed should give 200
49 200 Script output follows
58 200 Script output follows
50 % zip and tar.gz disallowed should both give 403
59 % zip and tar.gz disallowed should both give 403
51 403 Archive type not allowed: zip
60 403 Archive type not allowed: zip
52 403 Archive type not allowed: gz
61 403 Archive type not allowed: gz
53 $ test_archtype zip zip tar.gz tar.bz2
62 $ test_archtype zip zip tar.gz tar.bz2
54 % zip allowed should give 200
63 % zip allowed should give 200
55 200 Script output follows
64 200 Script output follows
56 % tar.gz and tar.bz2 disallowed should both give 403
65 % tar.gz and tar.bz2 disallowed should both give 403
57 403 Archive type not allowed: gz
66 403 Archive type not allowed: gz
58 403 Archive type not allowed: bz2
67 403 Archive type not allowed: bz2
59
68
69 check http return codes (with deprecated option)
70
71 $ test_archtype_deprecated gz tar.gz tar.bz2 zip
72 % gz allowed should give 200
73 200 Script output follows
74 % tar.bz2 and zip disallowed should both give 403
75 403 Archive type not allowed: bz2
76 403 Archive type not allowed: zip
77 $ test_archtype_deprecated bz2 tar.bz2 zip tar.gz
78 % bz2 allowed should give 200
79 200 Script output follows
80 % zip and tar.gz disallowed should both give 403
81 403 Archive type not allowed: zip
82 403 Archive type not allowed: gz
83 $ test_archtype_deprecated zip zip tar.gz tar.bz2
84 % zip allowed should give 200
85 200 Script output follows
86 % tar.gz and tar.bz2 disallowed should both give 403
87 403 Archive type not allowed: gz
88 403 Archive type not allowed: bz2
89
60 $ echo "allow_archive = gz bz2 zip" >> .hg/hgrc
90 $ echo "allow_archive = gz bz2 zip" >> .hg/hgrc
61 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
91 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
62 $ cat hg.pid >> $DAEMON_PIDS
92 $ cat hg.pid >> $DAEMON_PIDS
63
93
64 check archive links' order
94 check archive links' order
65
95
66 $ get-with-headers.py localhost:$HGPORT "?revcount=1" | grep '/archive/tip.'
96 $ get-with-headers.py localhost:$HGPORT "?revcount=1" | grep '/archive/tip.'
67 <a href="/archive/tip.zip">zip</a>
97 <a href="/archive/tip.zip">zip</a>
68 <a href="/archive/tip.tar.gz">gz</a>
98 <a href="/archive/tip.tar.gz">gz</a>
69 <a href="/archive/tip.tar.bz2">bz2</a>
99 <a href="/archive/tip.tar.bz2">bz2</a>
70
100
71 invalid arch type should give 404
101 invalid arch type should give 404
72
102
73 $ get-with-headers.py localhost:$HGPORT "archive/tip.invalid" | head -n 1
103 $ get-with-headers.py localhost:$HGPORT "archive/tip.invalid" | head -n 1
74 404 Unsupported archive type: None
104 404 Unsupported archive type: None
75
105
76 $ TIP=`hg id -v | cut -f1 -d' '`
106 $ TIP=`hg id -v | cut -f1 -d' '`
77 $ QTIP=`hg id -q`
107 $ QTIP=`hg id -q`
78 $ cat > getarchive.py <<EOF
108 $ cat > getarchive.py <<EOF
79 > from __future__ import absolute_import
109 > from __future__ import absolute_import
80 > import os
110 > import os
81 > import sys
111 > import sys
82 > from mercurial import (
112 > from mercurial import (
83 > util,
113 > util,
84 > )
114 > )
85 > try:
115 > try:
86 > # Set stdout to binary mode for win32 platforms
116 > # Set stdout to binary mode for win32 platforms
87 > import msvcrt
117 > import msvcrt
88 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
118 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
89 > except ImportError:
119 > except ImportError:
90 > pass
120 > pass
91 > if len(sys.argv) <= 3:
121 > if len(sys.argv) <= 3:
92 > node, archive = sys.argv[1:]
122 > node, archive = sys.argv[1:]
93 > requeststr = 'cmd=archive;node=%s;type=%s' % (node, archive)
123 > requeststr = 'cmd=archive;node=%s;type=%s' % (node, archive)
94 > else:
124 > else:
95 > node, archive, file = sys.argv[1:]
125 > node, archive, file = sys.argv[1:]
96 > requeststr = 'cmd=archive;node=%s;type=%s;file=%s' % (node, archive, file)
126 > requeststr = 'cmd=archive;node=%s;type=%s;file=%s' % (node, archive, file)
97 > try:
127 > try:
98 > stdout = sys.stdout.buffer
128 > stdout = sys.stdout.buffer
99 > except AttributeError:
129 > except AttributeError:
100 > stdout = sys.stdout
130 > stdout = sys.stdout
101 > try:
131 > try:
102 > f = util.urlreq.urlopen('http://$LOCALIP:%s/?%s'
132 > f = util.urlreq.urlopen('http://$LOCALIP:%s/?%s'
103 > % (os.environ['HGPORT'], requeststr))
133 > % (os.environ['HGPORT'], requeststr))
104 > stdout.write(f.read())
134 > stdout.write(f.read())
105 > except util.urlerr.httperror as e:
135 > except util.urlerr.httperror as e:
106 > sys.stderr.write(str(e) + '\n')
136 > sys.stderr.write(str(e) + '\n')
107 > EOF
137 > EOF
108 $ $PYTHON getarchive.py "$TIP" gz | gunzip | tar tf - 2>/dev/null
138 $ $PYTHON getarchive.py "$TIP" gz | gunzip | tar tf - 2>/dev/null
109 test-archive-1701ef1f1510/.hg_archival.txt
139 test-archive-1701ef1f1510/.hg_archival.txt
110 test-archive-1701ef1f1510/.hgsub
140 test-archive-1701ef1f1510/.hgsub
111 test-archive-1701ef1f1510/.hgsubstate
141 test-archive-1701ef1f1510/.hgsubstate
112 test-archive-1701ef1f1510/bar
142 test-archive-1701ef1f1510/bar
113 test-archive-1701ef1f1510/baz/bletch
143 test-archive-1701ef1f1510/baz/bletch
114 test-archive-1701ef1f1510/foo
144 test-archive-1701ef1f1510/foo
115 test-archive-1701ef1f1510/subrepo/sub
145 test-archive-1701ef1f1510/subrepo/sub
116 $ $PYTHON getarchive.py "$TIP" bz2 | bunzip2 | tar tf - 2>/dev/null
146 $ $PYTHON getarchive.py "$TIP" bz2 | bunzip2 | tar tf - 2>/dev/null
117 test-archive-1701ef1f1510/.hg_archival.txt
147 test-archive-1701ef1f1510/.hg_archival.txt
118 test-archive-1701ef1f1510/.hgsub
148 test-archive-1701ef1f1510/.hgsub
119 test-archive-1701ef1f1510/.hgsubstate
149 test-archive-1701ef1f1510/.hgsubstate
120 test-archive-1701ef1f1510/bar
150 test-archive-1701ef1f1510/bar
121 test-archive-1701ef1f1510/baz/bletch
151 test-archive-1701ef1f1510/baz/bletch
122 test-archive-1701ef1f1510/foo
152 test-archive-1701ef1f1510/foo
123 test-archive-1701ef1f1510/subrepo/sub
153 test-archive-1701ef1f1510/subrepo/sub
124 $ $PYTHON getarchive.py "$TIP" zip > archive.zip
154 $ $PYTHON getarchive.py "$TIP" zip > archive.zip
125 $ unzip -t archive.zip
155 $ unzip -t archive.zip
126 Archive: archive.zip
156 Archive: archive.zip
127 testing: test-archive-1701ef1f1510/.hg_archival.txt*OK (glob)
157 testing: test-archive-1701ef1f1510/.hg_archival.txt*OK (glob)
128 testing: test-archive-1701ef1f1510/.hgsub*OK (glob)
158 testing: test-archive-1701ef1f1510/.hgsub*OK (glob)
129 testing: test-archive-1701ef1f1510/.hgsubstate*OK (glob)
159 testing: test-archive-1701ef1f1510/.hgsubstate*OK (glob)
130 testing: test-archive-1701ef1f1510/bar*OK (glob)
160 testing: test-archive-1701ef1f1510/bar*OK (glob)
131 testing: test-archive-1701ef1f1510/baz/bletch*OK (glob)
161 testing: test-archive-1701ef1f1510/baz/bletch*OK (glob)
132 testing: test-archive-1701ef1f1510/foo*OK (glob)
162 testing: test-archive-1701ef1f1510/foo*OK (glob)
133 testing: test-archive-1701ef1f1510/subrepo/sub*OK (glob)
163 testing: test-archive-1701ef1f1510/subrepo/sub*OK (glob)
134 No errors detected in compressed data of archive.zip.
164 No errors detected in compressed data of archive.zip.
135
165
136 test that we can download single directories and files
166 test that we can download single directories and files
137
167
138 $ $PYTHON getarchive.py "$TIP" gz baz | gunzip | tar tf - 2>/dev/null
168 $ $PYTHON getarchive.py "$TIP" gz baz | gunzip | tar tf - 2>/dev/null
139 test-archive-1701ef1f1510/baz/bletch
169 test-archive-1701ef1f1510/baz/bletch
140 $ $PYTHON getarchive.py "$TIP" gz foo | gunzip | tar tf - 2>/dev/null
170 $ $PYTHON getarchive.py "$TIP" gz foo | gunzip | tar tf - 2>/dev/null
141 test-archive-1701ef1f1510/foo
171 test-archive-1701ef1f1510/foo
142
172
143 test that we detect file patterns that match no files
173 test that we detect file patterns that match no files
144
174
145 $ $PYTHON getarchive.py "$TIP" gz foobar
175 $ $PYTHON getarchive.py "$TIP" gz foobar
146 HTTP Error 404: file(s) not found: foobar
176 HTTP Error 404: file(s) not found: foobar
147
177
148 test that we reject unsafe patterns
178 test that we reject unsafe patterns
149
179
150 $ $PYTHON getarchive.py "$TIP" gz relre:baz
180 $ $PYTHON getarchive.py "$TIP" gz relre:baz
151 HTTP Error 404: file(s) not found: relre:baz
181 HTTP Error 404: file(s) not found: relre:baz
152
182
153 $ killdaemons.py
183 $ killdaemons.py
154
184
155 $ hg archive -t tar test.tar
185 $ hg archive -t tar test.tar
156 $ tar tf test.tar
186 $ tar tf test.tar
157 test/.hg_archival.txt
187 test/.hg_archival.txt
158 test/.hgsub
188 test/.hgsub
159 test/.hgsubstate
189 test/.hgsubstate
160 test/bar
190 test/bar
161 test/baz/bletch
191 test/baz/bletch
162 test/foo
192 test/foo
163
193
164 $ hg archive --debug -t tbz2 -X baz test.tar.bz2 --config progress.debug=true
194 $ hg archive --debug -t tbz2 -X baz test.tar.bz2 --config progress.debug=true
165 archiving: 0/4 files (0.00%)
195 archiving: 0/4 files (0.00%)
166 archiving: .hgsub 1/4 files (25.00%)
196 archiving: .hgsub 1/4 files (25.00%)
167 archiving: .hgsubstate 2/4 files (50.00%)
197 archiving: .hgsubstate 2/4 files (50.00%)
168 archiving: bar 3/4 files (75.00%)
198 archiving: bar 3/4 files (75.00%)
169 archiving: foo 4/4 files (100.00%)
199 archiving: foo 4/4 files (100.00%)
170 $ bunzip2 -dc test.tar.bz2 | tar tf - 2>/dev/null
200 $ bunzip2 -dc test.tar.bz2 | tar tf - 2>/dev/null
171 test/.hg_archival.txt
201 test/.hg_archival.txt
172 test/.hgsub
202 test/.hgsub
173 test/.hgsubstate
203 test/.hgsubstate
174 test/bar
204 test/bar
175 test/foo
205 test/foo
176
206
177 $ hg archive -t tgz -p %b-%h test-%h.tar.gz
207 $ hg archive -t tgz -p %b-%h test-%h.tar.gz
178 $ gzip -dc test-$QTIP.tar.gz | tar tf - 2>/dev/null
208 $ gzip -dc test-$QTIP.tar.gz | tar tf - 2>/dev/null
179 test-1701ef1f1510/.hg_archival.txt
209 test-1701ef1f1510/.hg_archival.txt
180 test-1701ef1f1510/.hgsub
210 test-1701ef1f1510/.hgsub
181 test-1701ef1f1510/.hgsubstate
211 test-1701ef1f1510/.hgsubstate
182 test-1701ef1f1510/bar
212 test-1701ef1f1510/bar
183 test-1701ef1f1510/baz/bletch
213 test-1701ef1f1510/baz/bletch
184 test-1701ef1f1510/foo
214 test-1701ef1f1510/foo
185
215
186 $ hg archive autodetected_test.tar
216 $ hg archive autodetected_test.tar
187 $ tar tf autodetected_test.tar
217 $ tar tf autodetected_test.tar
188 autodetected_test/.hg_archival.txt
218 autodetected_test/.hg_archival.txt
189 autodetected_test/.hgsub
219 autodetected_test/.hgsub
190 autodetected_test/.hgsubstate
220 autodetected_test/.hgsubstate
191 autodetected_test/bar
221 autodetected_test/bar
192 autodetected_test/baz/bletch
222 autodetected_test/baz/bletch
193 autodetected_test/foo
223 autodetected_test/foo
194
224
195 The '-t' should override autodetection
225 The '-t' should override autodetection
196
226
197 $ hg archive -t tar autodetect_override_test.zip
227 $ hg archive -t tar autodetect_override_test.zip
198 $ tar tf autodetect_override_test.zip
228 $ tar tf autodetect_override_test.zip
199 autodetect_override_test.zip/.hg_archival.txt
229 autodetect_override_test.zip/.hg_archival.txt
200 autodetect_override_test.zip/.hgsub
230 autodetect_override_test.zip/.hgsub
201 autodetect_override_test.zip/.hgsubstate
231 autodetect_override_test.zip/.hgsubstate
202 autodetect_override_test.zip/bar
232 autodetect_override_test.zip/bar
203 autodetect_override_test.zip/baz/bletch
233 autodetect_override_test.zip/baz/bletch
204 autodetect_override_test.zip/foo
234 autodetect_override_test.zip/foo
205
235
206 $ for ext in tar tar.gz tgz tar.bz2 tbz2 zip; do
236 $ for ext in tar tar.gz tgz tar.bz2 tbz2 zip; do
207 > hg archive auto_test.$ext
237 > hg archive auto_test.$ext
208 > if [ -d auto_test.$ext ]; then
238 > if [ -d auto_test.$ext ]; then
209 > echo "extension $ext was not autodetected."
239 > echo "extension $ext was not autodetected."
210 > fi
240 > fi
211 > done
241 > done
212
242
213 $ cat > md5comp.py <<EOF
243 $ cat > md5comp.py <<EOF
214 > from __future__ import absolute_import, print_function
244 > from __future__ import absolute_import, print_function
215 > import hashlib
245 > import hashlib
216 > import sys
246 > import sys
217 > f1, f2 = sys.argv[1:3]
247 > f1, f2 = sys.argv[1:3]
218 > h1 = hashlib.md5(open(f1, 'rb').read()).hexdigest()
248 > h1 = hashlib.md5(open(f1, 'rb').read()).hexdigest()
219 > h2 = hashlib.md5(open(f2, 'rb').read()).hexdigest()
249 > h2 = hashlib.md5(open(f2, 'rb').read()).hexdigest()
220 > print(h1 == h2 or "md5 differ: " + repr((h1, h2)))
250 > print(h1 == h2 or "md5 differ: " + repr((h1, h2)))
221 > EOF
251 > EOF
222
252
223 archive name is stored in the archive, so create similar archives and
253 archive name is stored in the archive, so create similar archives and
224 rename them afterwards.
254 rename them afterwards.
225
255
226 $ hg archive -t tgz tip.tar.gz
256 $ hg archive -t tgz tip.tar.gz
227 $ mv tip.tar.gz tip1.tar.gz
257 $ mv tip.tar.gz tip1.tar.gz
228 $ sleep 1
258 $ sleep 1
229 $ hg archive -t tgz tip.tar.gz
259 $ hg archive -t tgz tip.tar.gz
230 $ mv tip.tar.gz tip2.tar.gz
260 $ mv tip.tar.gz tip2.tar.gz
231 $ $PYTHON md5comp.py tip1.tar.gz tip2.tar.gz
261 $ $PYTHON md5comp.py tip1.tar.gz tip2.tar.gz
232 True
262 True
233
263
234 $ hg archive -t zip -p /illegal test.zip
264 $ hg archive -t zip -p /illegal test.zip
235 abort: archive prefix contains illegal components
265 abort: archive prefix contains illegal components
236 [255]
266 [255]
237 $ hg archive -t zip -p very/../bad test.zip
267 $ hg archive -t zip -p very/../bad test.zip
238
268
239 $ hg archive --config ui.archivemeta=false -t zip -r 2 test.zip
269 $ hg archive --config ui.archivemeta=false -t zip -r 2 test.zip
240 $ unzip -t test.zip
270 $ unzip -t test.zip
241 Archive: test.zip
271 Archive: test.zip
242 testing: test/bar*OK (glob)
272 testing: test/bar*OK (glob)
243 testing: test/baz/bletch*OK (glob)
273 testing: test/baz/bletch*OK (glob)
244 testing: test/foo*OK (glob)
274 testing: test/foo*OK (glob)
245 No errors detected in compressed data of test.zip.
275 No errors detected in compressed data of test.zip.
246
276
247 $ hg archive -t tar - | tar tf - 2>/dev/null
277 $ hg archive -t tar - | tar tf - 2>/dev/null
248 test-1701ef1f1510/.hg_archival.txt
278 test-1701ef1f1510/.hg_archival.txt
249 test-1701ef1f1510/.hgsub
279 test-1701ef1f1510/.hgsub
250 test-1701ef1f1510/.hgsubstate
280 test-1701ef1f1510/.hgsubstate
251 test-1701ef1f1510/bar
281 test-1701ef1f1510/bar
252 test-1701ef1f1510/baz/bletch
282 test-1701ef1f1510/baz/bletch
253 test-1701ef1f1510/foo
283 test-1701ef1f1510/foo
254
284
255 $ hg archive -r 0 -t tar rev-%r.tar
285 $ hg archive -r 0 -t tar rev-%r.tar
256 $ [ -f rev-0.tar ]
286 $ [ -f rev-0.tar ]
257
287
258 test .hg_archival.txt
288 test .hg_archival.txt
259
289
260 $ hg archive ../test-tags
290 $ hg archive ../test-tags
261 $ cat ../test-tags/.hg_archival.txt
291 $ cat ../test-tags/.hg_archival.txt
262 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
292 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
263 node: 1701ef1f151069b8747038e93b5186bb43a47504
293 node: 1701ef1f151069b8747038e93b5186bb43a47504
264 branch: default
294 branch: default
265 latesttag: null
295 latesttag: null
266 latesttagdistance: 4
296 latesttagdistance: 4
267 changessincelatesttag: 4
297 changessincelatesttag: 4
268 $ hg tag -r 2 mytag
298 $ hg tag -r 2 mytag
269 $ hg tag -r 2 anothertag
299 $ hg tag -r 2 anothertag
270 $ hg archive -r 2 ../test-lasttag
300 $ hg archive -r 2 ../test-lasttag
271 $ cat ../test-lasttag/.hg_archival.txt
301 $ cat ../test-lasttag/.hg_archival.txt
272 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
302 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
273 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
303 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
274 branch: default
304 branch: default
275 tag: anothertag
305 tag: anothertag
276 tag: mytag
306 tag: mytag
277
307
278 $ hg archive -t bogus test.bogus
308 $ hg archive -t bogus test.bogus
279 abort: unknown archive type 'bogus'
309 abort: unknown archive type 'bogus'
280 [255]
310 [255]
281
311
282 enable progress extension:
312 enable progress extension:
283
313
284 $ cp $HGRCPATH $HGRCPATH.no-progress
314 $ cp $HGRCPATH $HGRCPATH.no-progress
285 $ cat >> $HGRCPATH <<EOF
315 $ cat >> $HGRCPATH <<EOF
286 > [progress]
316 > [progress]
287 > assume-tty = 1
317 > assume-tty = 1
288 > format = topic bar number
318 > format = topic bar number
289 > delay = 0
319 > delay = 0
290 > refresh = 0
320 > refresh = 0
291 > width = 60
321 > width = 60
292 > EOF
322 > EOF
293
323
294 $ hg archive ../with-progress
324 $ hg archive ../with-progress
295 \r (no-eol) (esc)
325 \r (no-eol) (esc)
296 archiving [ ] 0/6\r (no-eol) (esc)
326 archiving [ ] 0/6\r (no-eol) (esc)
297 archiving [======> ] 1/6\r (no-eol) (esc)
327 archiving [======> ] 1/6\r (no-eol) (esc)
298 archiving [=============> ] 2/6\r (no-eol) (esc)
328 archiving [=============> ] 2/6\r (no-eol) (esc)
299 archiving [====================> ] 3/6\r (no-eol) (esc)
329 archiving [====================> ] 3/6\r (no-eol) (esc)
300 archiving [===========================> ] 4/6\r (no-eol) (esc)
330 archiving [===========================> ] 4/6\r (no-eol) (esc)
301 archiving [==================================> ] 5/6\r (no-eol) (esc)
331 archiving [==================================> ] 5/6\r (no-eol) (esc)
302 archiving [==========================================>] 6/6\r (no-eol) (esc)
332 archiving [==========================================>] 6/6\r (no-eol) (esc)
303 \r (no-eol) (esc)
333 \r (no-eol) (esc)
304
334
305 cleanup after progress extension test:
335 cleanup after progress extension test:
306
336
307 $ cp $HGRCPATH.no-progress $HGRCPATH
337 $ cp $HGRCPATH.no-progress $HGRCPATH
308
338
309 server errors
339 server errors
310
340
311 $ cat errors.log
341 $ cat errors.log
312
342
313 empty repo
343 empty repo
314
344
315 $ hg init ../empty
345 $ hg init ../empty
316 $ cd ../empty
346 $ cd ../empty
317 $ hg archive ../test-empty
347 $ hg archive ../test-empty
318 abort: no working directory: please specify a revision
348 abort: no working directory: please specify a revision
319 [255]
349 [255]
320
350
321 old file -- date clamped to 1980
351 old file -- date clamped to 1980
322
352
323 $ touch -t 197501010000 old
353 $ touch -t 197501010000 old
324 $ hg add old
354 $ hg add old
325 $ hg commit -m old
355 $ hg commit -m old
326 $ hg archive ../old.zip
356 $ hg archive ../old.zip
327 $ unzip -l ../old.zip | grep -v -- ----- | egrep -v files$
357 $ unzip -l ../old.zip | grep -v -- ----- | egrep -v files$
328 Archive: ../old.zip
358 Archive: ../old.zip
329 \s*Length.* (re)
359 \s*Length.* (re)
330 *172*80*00:00*old/.hg_archival.txt (glob)
360 *172*80*00:00*old/.hg_archival.txt (glob)
331 *0*80*00:00*old/old (glob)
361 *0*80*00:00*old/old (glob)
332
362
333 show an error when a provided pattern matches no files
363 show an error when a provided pattern matches no files
334
364
335 $ hg archive -I file_that_does_not_exist.foo ../empty.zip
365 $ hg archive -I file_that_does_not_exist.foo ../empty.zip
336 abort: no files match the archive pattern
366 abort: no files match the archive pattern
337 [255]
367 [255]
338
368
339 $ hg archive -X * ../empty.zip
369 $ hg archive -X * ../empty.zip
340 abort: no files match the archive pattern
370 abort: no files match the archive pattern
341 [255]
371 [255]
342
372
343 $ cd ..
373 $ cd ..
344
374
345 issue3600: check whether "hg archive" can create archive files which
375 issue3600: check whether "hg archive" can create archive files which
346 are extracted with expected timestamp, even though TZ is not
376 are extracted with expected timestamp, even though TZ is not
347 configured as GMT.
377 configured as GMT.
348
378
349 $ mkdir issue3600
379 $ mkdir issue3600
350 $ cd issue3600
380 $ cd issue3600
351
381
352 $ hg init repo
382 $ hg init repo
353 $ echo a > repo/a
383 $ echo a > repo/a
354 $ hg -R repo add repo/a
384 $ hg -R repo add repo/a
355 $ hg -R repo commit -m '#0' -d '456789012 21600'
385 $ hg -R repo commit -m '#0' -d '456789012 21600'
356 $ cat > show_mtime.py <<EOF
386 $ cat > show_mtime.py <<EOF
357 > from __future__ import absolute_import, print_function
387 > from __future__ import absolute_import, print_function
358 > import os
388 > import os
359 > import sys
389 > import sys
360 > print(int(os.stat(sys.argv[1]).st_mtime))
390 > print(int(os.stat(sys.argv[1]).st_mtime))
361 > EOF
391 > EOF
362
392
363 $ hg -R repo archive --prefix tar-extracted archive.tar
393 $ hg -R repo archive --prefix tar-extracted archive.tar
364 $ (TZ=UTC-3; export TZ; tar xf archive.tar)
394 $ (TZ=UTC-3; export TZ; tar xf archive.tar)
365 $ $PYTHON show_mtime.py tar-extracted/a
395 $ $PYTHON show_mtime.py tar-extracted/a
366 456789012
396 456789012
367
397
368 $ hg -R repo archive --prefix zip-extracted archive.zip
398 $ hg -R repo archive --prefix zip-extracted archive.zip
369 $ (TZ=UTC-3; export TZ; unzip -q archive.zip)
399 $ (TZ=UTC-3; export TZ; unzip -q archive.zip)
370 $ $PYTHON show_mtime.py zip-extracted/a
400 $ $PYTHON show_mtime.py zip-extracted/a
371 456789012
401 456789012
372
402
373 $ cd ..
403 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now